My OAuth Solution (Courtesy of Everyone Else)
About 3 days ago (as of writing this post) the OAuth session fixation vulnerability was made public. I'm not going to discuss the history of the vulnerability, the dramatic and gripping story of how Service Providers were notified and gathered together in a secret society that would rival the Illuminati to deal with the issue. I'm not going to discuss it because the movie, graphic novel and bestselling thriller book are all probably in the works.
The attack itself is quite simple, and relies not so much on weaknesses in the protocol itself but on weaknesses in the psychology of Homo Sapiens. What basically happens usually is this (note the following example is very specific to web application consumers, but OAuth flows exist in a few other forms too):
- Consumer gets a request token from the Service Provider, and prepares a link to the SP's User Authorization URL with the token and a callback as a query parameter.
- User clicks this link and goes to the SP where he/she/it authorizes the consumer to access their account.
- The SP then redirects them to the callback which was specified on the User Auth URL.
- The Consumer, which has just had its callback GET'd, now attempts to exchange that request token for an access token, which will now allow the Consumer to make authorized requests to the Service Provider on behalf of the User.
The session fixation attack looks like this:
- Malicious User (MU) visits the consumer and gets the link to the SP's User Auth URL, but instead of following it, modifies the callback parameter to a blank URL (like Google, or example.com), and tries to get Innocent User (IU) to click on it.
- IU grants access to the Consumer, as it appears the entire thing is kosher.
- IU is redirected to blank URL, whilst MU now visits the original callback URL. The Consumer will now try to exchange MU's request token for IU's access token, and this will succeed. MU will now have access to IU's account (but only through the Consumer). Because the MU has no idea when IU will authorize the Consumer, MU will probably poll the callback, waiting for the access token to be granted to the Consumer.
So at its heart, the problem comes in two parts:
- We need to make sure the MU cannot manipulate the callback URL.
- We need to make sure the MU cannot poll the callback URL.
- We need to make sure that the user who authorizes the Consumer is the same as the one trying to exchange the request token for the access token.
From hanging around on the appropriate thread on the OAuth mailing list (and contributing to it quite a bit myself), I've pretty much amalgamated a solution based on a few ideas.
- To ensure that the callback URL cannot be modified, the entire User Auth URL (callback and all) should be signed by the Consumer. This provides a way for the SP to be certain that the URL was generated by the Consumer (and only the Consumer) and was not modified at any point.
- To ensure that MU cannot poll the callback URL, a once-only rule should be mandatory; this means that if the Consumer tries to exchange the request token once and fails (because the user has not (yet) authorized it), any subsequent attempts will fail unconditionally. Thus, if MU does poll the callback URL, the first request will be the only one which even has a chance of working. The next and last point eliminates this, too.
- To ensure that the user who authorized the app is the same as the one who tries to exchange the request token, a callback token is required. When the user is redirected from the SP to the Consumer's callback URL, an additional 'oauth_callback_token' query parameter is added with a unique string generated by the SP upon authorization. When the Consumer attempts to exchange the request token for an access token, this parameter must be present in the request or the exchange will fail. Thus, even if MU does poll the callback URL, he/she/it will be unable to get access as he/she/it will not know this token.
A lot of other people on the list are proposing rather massive changes to the OAuth flow, but I like the approach I've laid out here because:
- It's simple.
- It works.
- It requires practically no change in the user flow for web applications.
- It keeps all the burden of signature/token verification and user authentication on the Service Provider. This is how it should be; the Consumer is a third-party and as such cannot be trusted.
So what do you think?