Web Security

What Is CSRF? Cross-Site Request Forgery Explained

A complete technical guide to Cross-Site Request Forgery (CSRF): how browsers get tricked into sending authenticated requests, and how to detect, verify, and prevent it.

Beginner to Intermediate12 min·Nyxeara Security Research·2026-09-16·CWE-352
csrfweb-securityauthenticationsession-security
Prerequisites
what-is-httphow-cookies-work

Short answer

Cross-Site Request Forgery (CSRF) tricks a victim's browser into sending a legitimate, authenticated request to a site the victim didn't mean to interact with. The request looks completely genuine to the server, because it is — the browser just sent it for the wrong reasons.

The idea in one minute

Picture a delivery worker who's been given a building keycard. The building's rule is simple: if the door beeps green for your card, whatever you're carrying gets delivered inside — no one checks who packed the box or why you're carrying it.

Now imagine a stranger hands the delivery worker a box on the way in and says "just drop this at the front desk, would you?" The worker's card still beeps green. The building still lets them in. The delivery still happens — but the worker never chose to bring that particular box, they were just an authenticated body walking through a door that only checks the badge.

That's CSRF. The browser is the delivery worker; the session cookie is the badge. Browsers automatically attach cookies to every request sent to a site, regardless of what triggered that request — a link you clicked on purpose, or a hidden form on a completely different website you happened to visit. If the server's only proof of legitimacy is "a valid cookie came with this request," it can't tell the difference between a user's actual intent and a request some other page silently made on their behalf.

How CSRF actually works

Three ingredients make it possible:

  1. The victim is authenticated to the target site (has a valid session cookie)
  2. The target site performs some state-changing action based on a request, using only the cookie as proof of authorization
  3. The browser will attach that cookie automatically to a request triggered from any page — including one the attacker controls

An attacker's page doesn't need to read anything back from the target site — it just needs to make the victim's browser fire a request. A hidden auto-submitting form, an <img> tag pointed at a state-changing GET endpoint, or a script triggering a fetch() call are all enough, because the browser doesn't ask "did the user mean to do this?" before attaching cookies — only "does this request go to a domain I have cookies for?"

| Variant | Mechanism | Notable feature | |---|---|---| | GET-based CSRF | A simple link or <img src="..."> triggers a state change via GET | Requires the vulnerable action to (improperly) accept GET requests | | POST-based CSRF | A hidden, auto-submitting HTML form on the attacker's page | Works against most typical form-based actions | | Login CSRF | Forces the victim to log into the attacker's account | Victim unknowingly saves data (searches, purchases) into an account the attacker controls |

The request flow

sequenceDiagram
    participant Victim
    participant AttackerSite as Attacker's Page
    participant Target as Target Application

    Victim->>Target: Logs in normally, receives session cookie
    Victim->>AttackerSite: Visits attacker's page (unrelated tab/link)
    AttackerSite-->>Victim: Page auto-submits hidden form to Target
    Victim->>Target: Browser sends forged request WITH the session cookie
    Target-->>Victim: Request processed as if the victim intended it

A minimal example

An email-change endpoint that trusts the session cookie alone:

@app.route("/change-email", methods=["POST"])
def change_email():
    new_email = request.form["email"]
    current_user.email = new_email
    db.session.commit()
    return "Email updated"

An attacker-hosted page that exploits it:

<form action="https://target.example.com/change-email" method="POST" id="csrf-form">
  <input type="hidden" name="email" value="attacker@evil.example">
</form>
<script>document.getElementById('csrf-form').submit();</script>

Any authenticated user who loads that page has their email silently changed — the request looks, to the server, exactly like a real one.

The fix adds a second proof of intent the attacker's page can't produce:

from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)

@app.route("/change-email", methods=["POST"])
def change_email():
    # CSRFProtect validates a per-session token before this code runs
    new_email = request.form["email"]
    current_user.email = new_email
    db.session.commit()
    return "Email updated"

The token is unpredictable and tied to the victim's session — the attacker's page has no way to know it, so the forged form can't include a valid one.

Why the vulnerability exists

  • State-changing actions rely on cookies alone as proof of authorization, with no additional token or check
  • SameSite is missing from the session cookie, or explicitly set to None, allowing it to be sent on cross-site requests
  • The server doesn't validate Origin or Referer headers as a sanity check
  • Anti-CSRF tokens exist in the codebase but aren't actually verified server-side on every state-changing route — a common gap when new endpoints are added after the original CSRF protection was implemented

What attackers look for

  • Any POST/PUT/DELETE endpoint that changes state (email, password, funds, settings, content) using cookie-based session auth
  • State-changing actions incorrectly implemented as GET requests
  • Forms without a visible or hidden CSRF token field
  • Cookies set without SameSite=Lax or SameSite=Strict

Detection

  • Manual/DAST: replay a captured state-changing request with the CSRF token parameter removed or altered — if the server still processes it, the token isn't actually being validated.
  • Cookie inspection: check the Set-Cookie header for the session cookie's SameSite attribute.
  • Cross-origin proof of concept: host a minimal auto-submitting form on a different origin and confirm whether the action executes against an authenticated session.

Verification: real vulnerability or false positive?

A missing token field alone isn't proof — some applications validate via custom headers that can't be set by a simple cross-site form (which blocks the classic attack even without a visible token), or via double-submit cookie patterns checked elsewhere. Confirm the state change actually occurs when the forged request is sent from a genuinely different origin with an authenticated session and no manually-added protection headers — that's the real test, not just the absence of a csrf_token input field.

Real-world impact

  • Unauthorized state changes: email/password changes, fund transfers, content posting, account settings modified without the victim's knowledge
  • Account takeover chains: forcing an email change followed by a password reset flow can hand over full account control
  • Home router attacks: a well-documented class of "drive-by pharming" attacks used CSRF against home routers' web admin panels — often left at default credentials and unprotected against forged requests — to silently change DNS settings and redirect victims' traffic
  • Privilege impact scales with the victim: CSRF against an administrator session can compromise application-wide settings, not just one account

Prevention

  • Synchronizer token pattern: generate an unpredictable, session-tied token; require it on every state-changing request; reject requests where it's missing or incorrect
  • SameSite=Lax (or Strict) on session cookies: stops the browser from attaching the cookie to most cross-site requests in the first place — a strong, low-effort baseline defense
  • Custom request headers for AJAX/API calls: a header like X-Requested-With can't be set by a plain cross-site form submission, adding a check the forged request can't satisfy
  • Origin/Referer validation as defense-in-depth, not a sole control — headers can be absent in some legitimate configurations, so pair this with a token
  • Never perform state changes via GET — GET requests are trivially triggered via <img> tags and should be side-effect-free by convention

Related vulnerabilities

  • XSS — can defeat CSRF protections entirely, since injected script runs with full access to read and submit whatever tokens the legitimate page has
  • Clickjacking — a related trickery pattern that also abuses a victim's authenticated session, via a hidden frame instead of a forged request
  • Open redirect — can be chained to make phishing/CSRF delivery links look more trustworthy
  • Authentication vulnerabilities — CSRF's impact is directly tied to what an authenticated session can do

Testing methodology (do this safely)

  • Only test targets you're authorized to test, using test accounts you control — never trigger real state changes on other users' accounts.
  • Confirm the finding by tampering with or removing the CSRF token on a captured request and replaying it, rather than assuming vulnerability from a missing form field alone.
  • Build a minimal cross-origin proof-of-concept page and confirm the state change actually fires from a genuinely different origin.
  • Document the exact request, the missing/bypassed control, and the confirmed state change.

Further reading

Nyxeara perspective

A CSRF finding isn't confirmed by a missing token field — it's confirmed by actually tampering with the token and replaying the request. Plenty of applications protect state-changing routes through headers or patterns that never show up as a visible form field, and treating "no token in the HTML" as the finding produces false positives that don't survive a second look.

Published 2026-09-16 · Updated 2026-09-16