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.
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:
- fixed for django https://github.com/omab/django-social-auth/issues/386
- fixed in omniauth https://github.com/intridea/omniauth/issues/612
- reported and fixed in soundcloud.com
- checking public libraries..
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.
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@blog, dude, thank you, I misused both terms! Going to swap it! fail :)
ReplyDeleteIMHO, 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.
ReplyDeleteWhat do you think?
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
Deletei 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?
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
ReplyDeleteI agree that the extra confirmation can be a nuisance for the user, however sometimes this extra step is preferable.
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.
DeleteBut 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.
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.
Deletethe attack is a bit longer.
Deletethere 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
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?
ReplyDeleteHowever can this still get spoofed?
state fixation? depends on session type - in rails it's signed and you can't replace state w/o breaking login
Deletei don't think it's similar. it's active only for 5 minutes
ReplyDelete"state should not be equal form_authenticity_token(session[:csrf_token])"
ReplyDeleteCould you expand on that? Why is it a bad idea to reuse form_authenticity_token as state?
Because you send it to external service and they can reuse it to hack your app with CSRF
DeleteHi Egor,
ReplyDelete"$_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?
SESSION[state] is empty in the beginning, so sending no 'state' passes the verification.
Delete