This article explains a critical OAuth implementation flaw: the missing or unverified state parameter that allows attackers to link their social identity to a victim's account. The lab demonstrates how an attacker can capture their own OAuth callback and redirect a victim's authenticated session to it, causing the victim's account to become linked to the attacker's social identity. This is a common real-world vulnerability listed in OWASP Top 10 and represents a direct account-takeover primitive.
- State parameter is CSRF protection for OAuthThe OAuth state parameter is a random, single-use token generated by the client, sent during the authorization request, and verified on the callback. It serves as a CSRF token proving that the callback belongs to the same user who initiated the flow. Without it, there is no mechanism to tie the callback to a specific user or session.
- Attack requires no state validationThe attacker begins their own OAuth link flow with the vulnerable application, captures the resulting callback URL containing their authorization code, then delivers that same callback to the victim's authenticated session. Since ConnectApp performs no state validation, it accepts the callback at face value and links the attacker's social identity to the victim's account.
- Account takeover results from callback injectionBy injecting a callback carrying the attacker's authorization code into the victim's authenticated session, the attacker's social identity becomes permanently linked to the victim's account. This is classified as a login-CSRF vulnerability and represents a complete account-takeover primitive that requires no additional exploitation steps.
- Common real-world implementation flawMissing or unverified state is documented as one of the most frequent OAuth implementation flaws and a recurring source of account-takeover vulnerability reports. The OAuth specification mandates state as a required anti-CSRF measure, and the flaw falls under OWASP Top 10 (2021) categories A01 Broken Access Control and A07 Identification and Authentication Failures.
- Defense requires state generation and verificationApplications must generate a random state value per OAuth flow, bind it to the user's session, and reject any callback with missing or mismatched state. Additional protections include implementing PKCE for the authorization-code flow and requiring re-authentication before sensitive account linking operations.
- Testing approach for security assessmentWhen authorized to test an OAuth flow, remove the state parameter and verify whether the callback is still accepted by the application. If accepted without state, the flow is vulnerable to login-CSRF and account linking abuse, indicating a critical implementation gap.
Bottom line: The OAuth state parameter is not optional—it is the only mechanism preventing attackers from hijacking callbacks to link their identity to a victim's account. Any OAuth implementation that omits or fails to validate state is immediately vulnerable to account takeover through callback injection, making state verification a mandatory security requirement.
state parameter on the way out, and never checks it on the way back. That missing value is exactly what stops an attacker from stitching their login onto your session — and without it, they can.📋 In this walkthrough
What this lab teaches
The OAuth state parameter is a CSRF token for the OAuth flow. The client generates it, sends it on the authorization request, and verifies it on the callback, so it knows the callback belongs to the same user who started the flow. Drop it and an attacker can feed a victim a callback carrying the attacker's authorization code — linking the attacker's identity to the victim's account.
The lesson: state is not optional. It is the only thing tying the callback to the user who began the flow.
How ConnectApp links a social account
ConnectApp starts the link without a state value and accepts any callback at face value. An attacker begins their own OAuth link, captures the resulting callback (which carries the attacker's code), and gets the victim's browser to visit it. ConnectApp, having nothing to compare, links the attacker's social identity to the victim's session.
The attack approach
- Confirm the authorization request has no state parameter and the callback is not checked.
- Begin an account-link flow yourself and capture the callback URL with your own code.
- Deliver that callback to the victim's authenticated session.
- The victim's account becomes linked to your social identity — an account takeover primitive.
Full walkthrough (spoiler)
If the callback is rejected, something is validating the flow after all — open the steps.
Show the step-by-step solution
- Start the Twitter link in ConnectApp as the attacker and stop at the callback, capturing the URL that carries your authorization code.
- Because there is no state, that callback is not bound to any particular user or session.
- Have the victim's logged-in session visit your captured callback (the lab simulates the victim following it).
- ConnectApp links your social account to the victim's account. The lab detects the cross-linked session and marks you solved.
Why it works
Without state, the callback carries a code but no proof of who started the flow. ConnectApp simply trusts whatever callback arrives in the victim's session and links the identity inside it. The attacker supplies that identity, so the victim's account ends up connected to the attacker — a classic login-CSRF / account-takeover.
Where this happens in the real world
Missing or unverified state is one of the most common OAuth implementation flaws and a frequent source of account-takeover reports, which is why the OAuth specification treats state as a required anti-CSRF measure. It falls under Broken Access Control (A01) and Identification and Authentication Failures (A07) in the OWASP Top 10 (2021).
How to defend against it
- Generate a random state value per flow, bind it to the user's session, and verify it on the callback.
- Reject any callback with a missing or mismatched state.
- Use PKCE as an additional protection for the authorization-code flow.
- Re-authenticate before sensitive linking so a stray callback cannot silently attach an identity.
Try it, then go further
Reading about it is the easy part. Open the lab, try your own approach before peeking at the spoiler, and see how many attempts it takes.
