Tuesday, July 3, 2012

The Most Common OAuth2 Vulnerability


HN discussion
TL;DR
If website uses OAuth multi-logins there is an easy way to log into somebody's account, protection is almost never implemented and people don't take into account that OAuth is also used for authentication.

OAuth2 is an authorization framework. Apparently it's very popular now. Disregards its popularity a lot of people don't understand it deeply enough to write proper and secure implementation.

OAuth1.a and OAuth2 are incompatible, some services use former(twitter, wtf, come on!), some latter, some of them have insufficient and poor documentation(in terms of security) etc. It took me a few hours to read OAuth2 draft thoroughly and I found a few interesting vectors. One of them I am exposing in this post.

It's really dangerous but very common vulnerability for multi-login OAuth websites. 
A little bit of theory:
  • response_type = code is server-side auth flow, should be used when possible, more secure than response_type = token. Provider returns 'code' with User's user-agent and Client sends along with client's credentials the code to obtain 'access_token'. Callback when user is redirected looks like site.com/oauth/callback?code=AQCOtAVov1Cu316rpqPfs-8nDb-jJEiF7aex9n05e2dq3oiXlDwubVoC8VEGNq10rSkyyFb3wKbtZh6xpgG59FsAMMSjIAr613Ly1usZ47jPqADzbDyVuotFaRiQux3g6Ut84nmAf9j-KEvsX0bEPH_aCekLNJ1QAnjpls0SL9ZSK-yw1wPQWQsBhbfMPNJ_LqI
  • I remind you, OAuth is all about authorization, not authentication. What's the difference, you might ask. OAuth just gives to Client access to User's resources on Provider.
    But very often Client authenticates you by 'profile_info' resource, thus we can call it authentication framework either.
Condition for the hacklogin with OAuth Provider + ability to add OAuth Provider logins in settings


Hacking, Step-by-step:
  • Choose Client which suits hack's "condition" - some site.com(we will use Pinterest as showcase) Start authentication process - click "Add OAuth Provider login". You need to get callback from Provider but should not visit it. It's quite difficult - all modern browsers redirect you automatically. I recommend bundle Firefox + NoRedirect extension.
  • Do not visit the last URL(http://pinterest.com/connect/facebook/?code=AQCOtAVov1Cu316rpqPfs-8nDb-jJEiF7aex9n05e2dq3oiXlDwubVoC8VEGNq10rSkyyFb3wKbtZh6xpgG59FsAMMSjIAr613Ly1usZ47jPqADzbDyVuotFaRiQux3g6Ut84nmAf9j-KEvsX0bEPH_aCekLNJ1QAnjpls0SL9ZSK-yw1wPQWQsBhbfMPNJ_LqI#_=_), just save and put it into <img src="URL"> or <iframe> or anything else you prefer to send requests.


  • Now all you need is to make the User(some certain target or random site.com user) to send HTTP request on your callback URL. You can force him to visit example.com/somepage.html which contains <iframe src=URL>, post <img> on his wall, send him an email/tweet, whatever. User must be logged in site.com when he sends the request.
    Well done, your oauth account is attached to User's account on site.com.
  • Voila, press Log In with that OAuth Provider - you are logged in directly to User's account on site.com.

    Enjoy: read private messages, post comments, change payment details, have lulz, whatever. In fact account is yours now.

    After you had enough fun you can just Disconnect that OAuth Provider and log out. Nobody will have an idea what has happened, you left no fingerprints!
How to detect, is certain OAuth implementation vulnerable?
If site doesn't send 'state' param and redirect_uri param is static and doesn't contain any random hashes - it's vulnerable.
I know at least 10+ popular vulnerable sites: e.g. pinterest, digg, soundcloud, snip.it, bit.ly, stumbleupon etc. If you know more sites - please drop me a line at homakov@gmail.com
Also all Rails + Omniauth are vulnerable. Have fun(about 23,300 results)

updates:



Mitigation:
You are supposed to send special optional param 'state' - any random hash you get back by Provider in User's callback: ?code=123&state=HASH. Before adding OAuth account you MUST verify session[state] is equal params[state].
Classic: Insecure-by-default means insecure. Majority of developers don't use it at all - nobody pays for "optional" weird param :trollface:
MUST READ SECTION.

Notices:
  • $_SESSION['state'] == $_REQUEST['state'] is vulnerable code, was used a while ago in FB examples. Emtpy string equals empty string.
  • I recommend you to filter 'code' in logs config.filter_parameters += [:code]
  • state should not be equal form_authenticity_token(session[:csrf_token]) in rails
  • if you implemented response_type=token flow w/o FB JS library, it's most likely vulnerable too.

At the moment I am working on "OAuth2 Security Proposal". The Proposal aims to make OAuth2 safer by changing its policies, rules and workflows. Most of the points are supposed to be backwards compatible but some of them are quite difficult to apply. Stay tuned, I am publishing it in a few days.

15 comments:

  1. While the threat you describe is real, you've got your terms backwards. OAuth is about *authorization* and _not_ *authentication*. Authentication is about verifying a user's identity, whereas authorization is about providing access to user data to a 3rd party.

    ReplyDelete
  2. @blog, dude, thank you, I misused both terms! Going to swap it! fail :)

    ReplyDelete
  3. IMHO, the original sin is in using a safe HTTP method (GET) to change state. The callback request should return a form asking the user to confirm the OAuth provider addition, eventually showing some user information obtained from that provider.
    What do you think?

    ReplyDelete
    Replies
    1. it is described as an option in drafts. for example if you set up posting to facebook from your twitter you will be asked - is it your facebook account

      i don't like this attitude - too many actions. accept on provider, accept on client. 'state' is quite good to make sure that you are you and this account on provider is yours. why to ask again?

      Delete
  4. Just as a matter of principle: avoid important state changing actions as a consequence of a GET method; always require a POST in response to an acknowledge form with CSRF protection
    I agree that the extra confirmation can be a nuisance for the user, however sometimes this extra step is preferable.

    ReplyDelete
    Replies
    1. yes, that is an absolute truth, we should avoid GET changing requests and require POST+CSRF protection for everything. At least 3 of my previous posts are about this subject.

      But you still can lie on user. For example if the hacker's account has his avatar, his first/last name so when he sees connect You You Your Avatar he still can click accept.

      Delete
    2. I'm not following you on the "still can lie on user". The ack. form with the CSRF token is generated by the legitimate client. The attacker would have to obtain this CSRF token.

      Delete
    3. the attack is a bit longer.
      there is User1, I create anoter account with same avatar/details of User1 - User2.
      I open an iframe or new window with src=site.com/callback?code=mycodeforUser2
      It will still *require* click from User1 to assign my account User2 because of CSRF token but some users are naive.
      Nevermind, just thought

      Delete
  5. Would it work if you set the state hash on the cookie itself? Then when the provider redirected back to you, you can compare between the cookie's state and the get parameter's state?

    However can this still get spoofed?

    ReplyDelete
    Replies
    1. state fixation? depends on session type - in rails it's signed and you can't replace state w/o breaking login

      Delete
  6. i don't think it's similar. it's active only for 5 minutes

    ReplyDelete
  7. "state should not be equal form_authenticity_token(session[:csrf_token])"

    Could you expand on that? Why is it a bad idea to reuse form_authenticity_token as state?

    ReplyDelete
    Replies
    1. Because you send it to external service and they can reuse it to hack your app with CSRF

      Delete
  8. Hi Egor,

    "$_SESSION['state'] == $_REQUEST['state'] is vulnerable code, was used a while ago in FB examples. Emtpy string equals empty string."

    Could you expand on that? Why exactly is this vulnerable code?

    ReplyDelete
    Replies
    1. SESSION[state] is empty in the beginning, so sending no 'state' passes the verification.

      Delete