Skip to content
mediumCVSS 5.4CWE-384A07:2021 – Identification and Authentication Failures

Login CSRF and Session Fixation

Forcing the victim into the attacker's session, the OAuth state parameter, callback CSRF, and account-linking takeover.

CSRF pointed at the login form

Login CSRF inverts the usual direction. Instead of performing an action as the victim, the attacker logs the victim into the attacker's account.

That sounds harmless — the attacker is giving away access to their own account. It is not, because the victim does not notice, and everything they do next happens in the attacker's account, under the attacker's later control:

  • Search and browsing history accumulates in the attacker's account.
  • A saved payment method ends up on the attacker's account, usable by them.
  • Uploaded documents land in an attacker-owned drive.
  • A linked identity — if the victim connects their Google or GitHub account to what they think is their own account, the attacker now has an account that authenticates as the victim's identity provider. That is the takeover variant, and it is the one worth chasing.

The reason login forms are exposed is structural: many frameworks skip CSRF protection on login because there is no session yet to bind a token to. That reasoning is wrong — a pre-session token works fine.

The basic attack

HTMLattacker.example/index.htmlVulnerable
<!doctype html>
<html>
  <body>
    <!-- The attacker's own, real credentials. Nothing is being stolen at this
         point -- the victim is simply being logged in as someone else.

         The subtlety that makes this work in practice: many sites show only
         an avatar or a first name in the corner, so a victim who was already
         logged in may not notice they are now someone else. -->
    <form id="poc" action="https://app.example/login" method="POST">
      <input type="hidden" name="username" value="attacker@evil.example" />
      <input type="hidden" name="password" value="attackers-own-password" />
    </form>
    <script>document.getElementById('poc').submit()</script>
  </body>
</html>

Session fixation

The related failure, and the more serious one. Session fixation is when the session identifier does not change across the authentication boundary.

The attack:

  1. The attacker obtains a session identifier — usually by simply visiting the site and reading their own.
  2. They plant it in the victim's browser. From a same-site position this is one line of JavaScript; see Subdomain Cookie Injection. It can also be done through a URL parameter if the application accepts session IDs there, or through response splitting.
  3. The victim logs in normally, with their own credentials.
  4. The session ID does not change, so the attacker's known identifier is now an authenticated session belonging to the victim.

The attacker ends up holding a valid session for the victim's account. Unlike login CSRF, this is a direct account takeover.

The fix is one line in any framework: regenerate the session identifier on login, and on any privilege change. Rotate the CSRF token at the same time — a token that survives login is its own version of this bug.

OAuth: the state parameter and callback CSRF

HTMLVulnerable
<!-- OAuth's callback endpoint is a GET with a code in the query string. If
     the flow does not validate `state`, an attacker can feed the victim a
     callback carrying the ATTACKER'S authorization code.

     Step 1: the attacker begins a real OAuth flow with the provider, gets
             as far as the redirect, and captures their own `code` without
             letting the browser follow it.

     Step 2: they send the victim to the target's callback with that code.
             The target exchanges it, gets the ATTACKER's identity from the
             provider, and links or logs the victim into the attacker's
             account.

     A top-level GET navigation, so a Lax cookie rides along and this works
     in every browser except Strict. -->
<script>
  location = 'https://app.example/oauth/callback?code=ATTACKER_AUTH_CODE'
</script>

<!-- The account-linking variant is the serious one. If the victim is already
     logged into their own account and this callback LINKS rather than logs
     in, the attacker's identity-provider account is now attached to the
     victim's app account -- and the attacker can log in as the victim from
     then on, permanently. That is a full takeover and rates far above the
     5.4 on this page. -->

Regenerating the session

JavaScriptauth.jsVulnerable
app.post('/login', async (req, res) => {
  const user = await authenticate(req.body.username, req.body.password)
  if (!user) return res.status(401).send('Invalid credentials')

  // The session ID is unchanged from before authentication. Whatever
  // identifier the browser was carrying -- including one an attacker planted
  // -- is now an authenticated session.
  req.session.userId = user.id

  // The CSRF token is unchanged too, so an attacker who knew the
  // pre-authentication token still knows the post-authentication one.
  res.redirect('/dashboard')
})

Testing

  • Does the login form carry a CSRF token? Delete it and replay. Many applications skip protection here deliberately.
  • Does the session ID change on login? Record the session cookie before and after authenticating. If it is identical, that is session fixation, and it is a higher-severity finding than the login CSRF.
  • Does the CSRF token change on login? Same test.
  • Does the OAuth flow send state? Look at the authorize redirect. If there is no state parameter, the callback is forgeable.
  • Is state actually validated? Present is not the same as checked. Remove it, then alter it, and replay the callback both ways.
  • Is state single-use? Replay a whole valid callback twice.
  • Does the callback link accounts while authenticated? This is the path to full takeover — check whether hitting the callback as a logged-in user attaches a new identity rather than switching accounts.
  • Does logout regenerate? A session ID that survives logout is reusable.
  • Is there any "remember me" token that survives regeneration and re-establishes the old identity?