Back to Research

OAuth Session Token Flaw Leading to Full Account Takeover

A flaw in OAuth session token handling that allowed full account takeover with no user interaction.

Authentication bugs are among the most critical vulnerabilities you can find. They open the door to unauthorised access and, at their worst, let an attacker act entirely as another user.

This writeup covers a flaw in how an application handled OAuth session tokens. The session was validated, but the identity bound to it was not, resulting in a full account takeover with no user interaction.

#The OAuth flow

Login consisted of two steps.

Step 1. The user selects "Log in with Google" and the application sends a POST to /auth/login/google-v3, exchanging the user's Google token for a token the application uses to identify them.

The initial token exchange request to /auth/login/google-v3

Step 2. The application sends a second POST to /oauth2/exchange, trading that token for an access_token. This is the credential that unlocks every feature and all personal data.

The second exchange returning the access_token

Two steps, tokens moving back and forth, and it looks sound. A lot can still go wrong.

#The structure of the session token

The token returned by Step 1, then used in Step 2 to obtain the access_token, followed a structure that initially looked complex enough to be safe:

text
UKP!75ac38XX-XXXX-XXXX-XXXX-XXX144f8cbf4!e3f1f9XX-XXX-XXXX-XXXX-XXX1f9835e034

It is made of two parts:

  • First part. The USER_ID, identifying the user.
  • Second part. A unique session value tying that session to the identity.

Which raises the question worth asking of any composite token: is the server validating both parts, or only one?

#Look beyond URLs and request bodies

When testing for Insecure Direct Object Reference, it is easy to assume the manipulation happens in the obvious places, a URL parameter or a JSON body field. That is the tip of the iceberg.

The interesting cases often sit in headers and cookies, which get far less attention during testing. Here, the way in was the Authorization header.

If there is an identifier you can influence, test it wherever it lives.

#The bug

During login, the application validated only the second part of the session token, the unique session value. The USER_ID was decorative.

Since the server never checked that the session value belonged to the user it was paired with, I replaced the USER_ID in the Authorization header with another user's. The application accepted it and returned that user's access_token.

The modified Authorization header returning another user's access_token

#Root cause

The application validated the session but never the authority of the user that session belonged to. Any valid session value was accepted regardless of which identity accompanied it, so impersonating another account required nothing more than editing a header.

The fix is to bind the session value to its USER_ID server-side and reject any request where the two do not correspond, rather than trusting a client-supplied identifier that sits beside a credential the server does verify.