Skip to content

CSRF (Cross-Site Request Forgery)

Forces an authenticated victim’s browser to send an unwanted request to an app where they have a session. The attacker never sees the response; they abuse the fact that the browser automatically attaches credentials (cookies) to any request aimed at that site, from whatever origin. If a state-changing action can be replayed from its structure alone and the app trusts only the cookie, it’s vulnerable. In essence, CSRF is an intent-verification failure: the app doesn’t check that the user meant to make the request.

sequenceDiagram
    participant U as Victim (active session)
    participant M as Attacker site
    participant S as Target app
    U->>M: visits the malicious page
    M-->>U: auto-submitting form to S
    U->>S: request carrying the session cookies
    S-->>U: action executed as the victim

Impact equals the action forced with the victim’s session:

  • Change email or password → the attacker takes over the account (ATO) without knowing the current password.
  • Transfers, purchases, plan changes.
  • Create users or API keys, change permissions.
  • Modify persistent rules: mail forwarding, webhooks, router DNS, account recovery.
  • Login CSRF: force the victim to authenticate with the attacker’s account to capture their activity (searches, saved cards).

Three necessary conditions:

  1. A relevant action worth triggering.
  2. Cookie-based session the browser sends on its own (not header tokens that JS must add).
  3. Predictable parameters: no secret the attacker can’t guess.

By design, the browser attaches the target’s cookies on any cross-site request. The SameSite attribute modulates this:

  • None: always sent (requires Secure).
  • Lax (modern browser default): sent on top-level GET navigations (clicking a link), but not on cross-site POST or subresources.
  • Strict: not sent on any cross-site request.

Also, JSON requests with Content-Type: application/json trigger a CORS preflight, which blocks classic CSRF… unless the endpoint accepts “simple” Content-Types (text/plain, application/x-www-form-urlencoded).

  • GET with effects: a tag that loads the URL is enough (<img>, <link>).
  • POST: a self-submitting form.
  • JSON/APIs: exploitable if the server accepts text/plain/form or doesn’t validate the type (avoids preflight).
  • Multipart (enctype=multipart/form-data): for endpoints expecting files.
  • Login CSRF: authenticate the victim with the attacker’s credentials.
  • CSRF → XSS: when the forced action introduces a stored payload.
  • CORS/clickjacking-assisted CSRF: read the token (lax CORS) or complete a flow (clickjacking) when a parameter is missing.
  1. Capture every state-changing action and check for an anti-CSRF token (hidden field, header, mirror cookie).
  2. If present, systematically try to:
    • Remove it entirely.
    • Leave it empty.
    • Use a token from another session / account (is it session-bound?).
    • Use a valid but expired token, or one with the correct structure.
    • Switch POST→GET (many frameworks accept both).
    • Change the Content-Type (JSON → text/plain) to dodge preflight.
  3. Check the session cookie’s SameSite and whether the app validates Origin/Referer.
<!-- 1) Self-submitting POST -->
<form action="https://bank.tld/email/change" method="POST" id="x">
<input type="hidden" name="email" value="attacker@evil.tld">
</form>
<script>document.getElementById('x').submit()</script>
<!-- 2) GET with effects -->
<img src="https://bank.tld/transfer?to=attacker&amount=1000">
<!-- 3) JSON API without preflight (text/plain) -->
<form action="https://api.tld/v1/role" method="POST" enctype="text/plain">
<input name='{"role":"admin","x":"' value='"}'>
</form>
<script>document.forms[0].submit()</script>
<!-- 4) cross-site fetch (no-cors, for simple endpoints) -->
<script>
fetch('https://bank.tld/email/change',{method:'POST',mode:'no-cors',
credentials:'include',headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:'email=attacker@evil.tld'});
</script>

Host the PoC on your domain and get the victim to open it (link, iframe, ad).

  • Default SameSite=Lax: doesn’t cover top-level GET navigations. If a state-changing action accepts GET, it’s still exploitable with a plain link. It also poorly covers the first two minutes after the cookie is issued (historic window in some browsers) and the jump between subdomains/sibling sites.
  • Method override: APIs accepting _method=POST or X-HTTP-Method-Override let you send as GET/form something meant to be POST.
  • Token not session-bound: if the token is valid across accounts, steal it from your own session and reuse it in the victim’s.
  • Predictable / static token: if not random per session, it’s guessable.
  • Weak double-submit: if validation compares cookie vs parameter and you can set the cookie (via a subdomain you control or an injection), you bypass the check.
  • CORS misconfiguration: if the app reflects Origin with Allow-Credentials: true, read the token with an authenticated fetch and send it yourself.
  • Bypassable Referer validation: when it only checks “is there a Referer”, omit it with Referrer-Policy: no-referrer (meta) or a data: context.

Burp Suite (Engagement tools → Generate CSRF PoC), PoC-generation extensions, and curl to validate token absence/omission.

Direct account takeover (email/password change), privileged actions, persistence via rules (forwarding, webhooks), and CSRF→XSS→larger compromise. A login CSRF can capture sensitive data the victim enters thinking they’re in their own account.

  • State-changing requests with absent or external Origin/Referer.
  • Patterns of missing/invalid/reused token on sensitive endpoints.
  • Anomalous cross-site referers to critical actions; coordinated spikes from one campaign (same malicious link).
  • Email/password changes followed by activity from a new IP.

Logs with Origin/Referer/Content-Type per request, WAF, and sensitive-action metrics per user/origin.

  1. Anti-CSRF token (synchronizer token): unpredictable value, session-bound, mandatory and server-validated on every state-changing action. Alternative: well-implemented double-submit (token in cookie + header, compared), robust only if the attacker can’t set the cookie.
  2. SameSite cookies: Lax by default mitigates, but for critical actions use Strict and a token; don’t rely on the default alone.
  3. Validate Origin/Referer server-side for requests with effects (reject if it doesn’t match your own origin).
  4. APIs: require a custom header (X-Requested-With) or Authorization: Bearer (not auto-attached) and reject simple Content-Types on JSON endpoints (forcing application/json triggers preflight).
  5. Re-authentication / step-up for very sensitive actions (email change, transfers, account deletion).

Invalidate affected sessions, revert forced changes (email/rules/passwords), notify the victim, and add a token/Origin validation to the vulnerable endpoint.

  • ING Direct (2008) — Zeller & Felten’s academic paper demonstrated CSRF enabling transfers in online banking; a foundational case study.
  • YouTube (2008) — CSRF allowing multiple actions on the victim’s account (favorites, subscriptions, messages).
  • Home routers — waves of CSRF changing the router’s DNS from a malicious page (e.g. campaigns against routers in Brazil and the UK), redirecting all of the victim’s traffic.
  • CMS plugins (WordPress, etc.) — CSRF (often chained with stored XSS) is one of the most frequent CVE types in plugins.

Product-specific CVEs in NVD (https://nvd.nist.gov/vuln/search) and GitHub Advisories (https://github.com/advisories).

  • All state-changing actions identified.
  • Token presence and validation checked (remove/empty/other session/reuse).
  • POST→GET conversion and Content-Type change (preflight) tested.
  • SameSite and Origin/Referer validation reviewed.
  • Method override and double-submit with a set cookie tested.
  • CORS evaluated as a way to steal the token.
  • Working PoC for at least one impactful action (ideally ATO).