CSRF (CWE-352, OWASP A01:2021) forces authenticated users to execute unwanted state-changing requests by exploiting browser cookie auto-send.
TL;DR
Cross-Site Request Forgery is an attack in which an adversary tricks an authenticated user's browser into submitting a state-changing HTTP request to a web application the victim is logged into. The attacker does not need to steal the session cookie — the browser delivers it automatically. Three conditions must all be true for the attack to succeed: (1) a privileged action exists on the target — a password change, fund transfer, admin setting; (2) the application relies exclusively on session cookies with no supplementary verification; (3) all request parameters are predictable or controllable by the attacker in advance.
CSRF is classified as CWE-352 (Cross-Site Request Forgery). It appears under OWASP A01:2021 (Broken Access Control) when it bypasses access control enforcement on state-changing endpoints. It was removed as a standalone OWASP Top 10 category in 2021 — not because it is solved, but because SameSite defaults in modern browsers reduced prevalence on standard apps. Applications without SameSite-aware session cookies or explicit tokens remain fully exploitable in 2025-2026.
CSRF is distinct from XSS: XSS injects script that reads data and can steal tokens; CSRF forges outbound requests without reading anything. CSRF is distinct from SSRF: SSRF weaponizes the server's own network position; CSRF weaponizes the victim's browser session. CSRF is distinct from Clickjacking: Clickjacking overlays a transparent UI element to hijack the victim's own click; CSRF triggers the request invisibly without any click.
The attack exploits the browser's automatic cookie attachment to requests. A hidden form or resource request on the attacker's page causes the victim's browser to send a request to the target application — complete with the victim's session cookie — before the victim has any opportunity to notice.
The attack unfolds in four steps:
A concrete POST-based CSRF payload triggering a password change:
POST /account/change-password HTTP/1.1
Host: target.example.com
Content-Type: application/x-www-form-urlencoded
Cookie: session=abc123xyz
new_password=P%40ssw0rd_attacker&confirm_password=P%40ssw0rd_attackerThe attacker's page that generates this:
<!-- Auto-submits on page load — no user interaction required -->
<form id="csrf" action="https://target.example.com/account/change-password"
method="POST" style="display:none;">
<input name="new_password" value="P@ssw0rd_attacker">
<input name="confirm_password" value="P@ssw0rd_attacker">
</form>
<script>document.getElementById("csrf").submit();</script>| Variant | Technique | Impact | Dedicated Page |
|---|---|---|---|
| Classic GET-based | <img>, window.location, <a> — GET state-change via top-level nav | Fund transfer, account deletion | /learn/csrf-get |
| POST-based (Form) | Auto-submit hidden form with form-urlencoded or multipart/form-data | Password change, email change, admin action | /learn/csrf-post |
| JSON CSRF | enctype="text/plain" form sends JSON body; server parses regardless of Content-Type | API state mutation, GraphQL mutation | /learn/csrf-json |
| SameSite Bypass | Lax+POST 2-min window, method override (_method=POST), client-side redirect gadget, sibling-domain XSS | Bypasses the primary modern defense | /learn/csrf-samesite-bypass |
| Login CSRF | Force victim to authenticate as attacker's account — no token on pre-auth endpoint | Data harvesting, OAuth account linkage | /learn/csrf-login |
| OAuth State CSRF | Missing or static state parameter in OAuth flow — code injection into victim session | Account takeover via linked social identity | /learn/csrf-login |
GET-based CSRF targets the subset of applications that perform state changes on GET requests (violating RFC 9110 §9.2). SameSite=Lax cookies are sent on top-level GET navigations — a window.location redirect or <a href> click is sufficient. <img> tags trigger subresource GETs to which Lax cookies are not attached; the actual bypass vector is top-level navigation.
JSON CSRF uses enctype="text/plain" to construct a form body that reads as valid JSON. The browser sends Content-Type: text/plain — a CORS "simple" type that triggers no preflight — while the body payload is {"action":"deleteAccount","id":123,"x":"="}. Servers that parse raw body content without validating the Content-Type header execute the mutation. CVE-2024-4994 (GitLab GraphQL) and CVE-2025-68604 (WPGraphQL) both follow this pattern.
SameSite is the primary browser-enforced CSRF defense. Understanding exactly what it blocks — and what it does not — is critical for both offensive testing and defensive configuration.
| Cookie SameSite | Cross-site POST form | Top-level GET nav | <img> subresource GET | Notes |
|---|---|---|---|---|
Strict | Blocked | Blocked | Blocked | Safest — may break OAuth/SSO back-button flows |
Lax (Chrome 80+ default) | Blocked | Vulnerable | Blocked | State-changing GETs remain exposed |
Lax (implicit, new cookie, ≤120 s) | Vulnerable | Vulnerable | Blocked | Chrome grace window — see below |
None; Secure | Vulnerable | Vulnerable | Vulnerable | Required for cross-site embeds (payment widgets, OAuth) |
| Absent (legacy pre-Chrome 80) | Vulnerable | Vulnerable | Vulnerable | Chrome 80+ applies Lax by default for new cookies |
Cookie prefix hardening provides an additional layer against subdomain-injection attacks:
| Prefix | Requirements | Protection |
|---|---|---|
__Host- | Secure + Path=/ + no Domain attribute | Cannot be set from subdomains — strongest |
__Secure- | Secure flag only | Prevents HTTP cookie injection |
| None | No constraints | Vulnerable to subdomain cookie injection |
Optimal session cookie: Set-Cookie: __Host-SID=<token>; Path=/; Secure; HttpOnly; SameSite=Strict
Only 3.52% of websites implement any SameSite attribute (2024 audit). This means 96%+ of web applications depend entirely on CSRF tokens as their only defense.
SameSite=Lax 120-second grace window: Chrome applies a 2-minute Lax+POST exception to cookies issued without an explicit SameSite attribute. An attacker who forces re-authentication — by opening a logout URL in a popup, then immediately submitting the CSRF payload — exploits this window. The newly-issued session cookie has no SameSite enforcement for 120 seconds. Mitigation: use an explicit SameSite=Strict attribute; never omit SameSite.
Method override bypass (SameSite=Lax): Frameworks including Symfony, Rails, and Laravel accept _method=POST or X-HTTP-Method-Override: POST in GET requests. An attacker sends a GET with ?_method=POST&email=attacker@evil.com — the browser attaches Lax cookies to the GET, and the framework executes it as a POST. PortSwigger Lab: "SameSite Lax bypass via method override."
Client-side redirect gadget (SameSite=Strict bypass): Client-side redirects (JavaScript window.location = url_param) execute within the target origin — the browser treats the subsequent request as same-site, not cross-site. SameSite=Strict cookies are sent. An open redirect at https://target.com/redirect?url=/account/email?email=attacker@x.com exploits this.
Sibling domain attack: Same-site is defined at the eTLD+1 level. A subdomain XSS on assets.target.com can forge requests to app.target.com that are treated as same-site — bypassing SameSite=Strict and SameSite=Lax entirely. This also defeats naive double-submit cookie patterns because the attacker can inject a matching cookie via the compromised subdomain.
Token omission bypass: If a server validates the CSRF token only when the field is present (rather than requiring its presence), removing the csrf_token / authenticity_token / _token field from the form entirely bypasses protection. CVE-2023-47640 (Grails Framework, CVSS 8.8) exposed exactly this pattern.
Null Origin via sandboxed iframes: A sandboxed iframe with allow-scripts but no allow-same-origin sends Origin: null. If the server has logic if (!origin) allow(), CSRF succeeds despite SameSite=Lax.
CVE-2024-23897 — Jenkins CLI Arbitrary File Read → CSRF Crumb Forgery (CVSS 9.8, CISA KEV)
Jenkins ≤ 2.441 and LTS ≤ 2.426.2. The args4j CLI parser replaces @<filepath> arguments with file contents by default. Unauthenticated attackers read /var/jenkins_home/secrets/master.key and the CSRF crumb secret, then forged valid CSRF tokens to execute privileged Jenkins CLI actions without user interaction. Attack chain: file read → extract crumb → forge CSRF token → RCE. Added to CISA KEV catalog August 2024, due date September 9, 2024. Fixed in Jenkins 2.442 / LTS 2.426.3.
CVE-2024-20252 / CVE-2024-20254 — Cisco Expressway CSRF (CVSS 9.6 Critical) Cisco Expressway-C and Expressway-E, all releases before 14.3.41 and 15.0.01. Insufficient CSRF protections in the web management API allowed an unauthenticated remote attacker to trick an administrator into following a crafted link, executing arbitrary actions at admin privilege level — including system configuration changes and creation of privileged accounts. CVE-2024-20254 exploitable in default configuration; no workaround available, patch required. Published February 2024.
CVE-2024-4994 — GitLab GraphQL CSRF (CVSS 8.1, CWE-352) GitLab CE/EE 16.1.0 through 17.1.0. The GraphQL API accepted CSRF-exploitable content types (application/x-www-form-urlencoded, multipart/form-data) without preflight requirement, allowing an unauthenticated attacker to execute arbitrary GraphQL mutations on behalf of an authenticated victim. Impact: C:H/I:H. HackerOne #1122408 ($3,370 bounty) documented the GET mutation path. CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N.
CVE-2024-21690 — Atlassian Confluence CSRF + Reflected XSS (CVSS 8.2) Confluence Data Center and Server 7.19.0 through 8.9.0. Combined CSRF and reflected XSS vulnerability allowing unauthenticated attackers to execute arbitrary HTML/JavaScript and compel authenticated users to perform unintended state-changing actions. Fixed in 7.19.26+, 8.5.14+, 9.0.1+.
CVE-2024-49038 — Microsoft Copilot Studio PostMessage CSRF (CVSS 9.6)
Improper origin validation in Microsoft Copilot Studio's postMessage handler allowed privilege escalation via crafted messages from a cross-origin page. An attacker controls a page in an iframe or popup, posts a message to the Copilot Studio window without specifying targetOrigin, and the receiver executes the action without validating event.origin. Published November 26, 2024.
CVE-2025-68604 — WPGraphQL CSRF (CVSS 5.4) WPGraphQL ≤ 2.5.3. A remote page causes the victim's authenticated browser to submit a request to the GraphQL endpoint, executing mutations on behalf of the victim — creating unauthorized user accounts, modifying content, escalating privileges. Fixed in WPGraphQL 2.5.4. Published May 2026.
CVE-2022-24734 — MyBB Forum CSRF → RCE (CVSS 8.8)
MyBB < 1.8.30. CSRF on the admin panel file upload action combined with SameSite=None on the admin session cookie and absent CSRF token validation. Impact: remote code execution via attacker-controlled file write.
HackerOne #1497169 — GitHub Enterprise Management Console CSRF ($10,000) CSRF protection bypass in GitHub Enterprise's server management console enabling arbitrary actions on enterprise infrastructure at admin privilege level. Highest known CSRF bounty on HackerOne.
HackerOne #834366 — HackerOne.com Login CSRF ($500)
The authenticity_token on HackerOne's own login page was not properly verified server-side, allowing login via CSRF without a valid token — demonstrating that even security-focused platforms missed pre-authentication CSRF protection.
HackerOne #2326194 — Argo CD CSRF → Kubernetes Cluster Compromise ($4,660) Insufficient request validation in Argo CD's GitOps management interface allowed CSRF to affect the entire Kubernetes cluster configuration — full cluster compromise via a single CSRF exploit.
SameSite attribute, look for __Host- or __Secure- prefixes. Absence of SameSite on Chrome 80+ = implicit Lax (2-min window still applies for new cookies).csrf_token=) — accepted or rejected?application/json to text/plain on JSON API endpoints. Observe if the server parses the body regardless of Content-Type.application/x-www-form-urlencoded Content-Type.state parameter; test with absent, blank, and static state values.Origin header to a cross-origin value, verify server rejects it.Burp Suite Pro includes a CSRF PoC generator and active scanner rule. OWASP ZAP active scan rule 20012 (Anti-CSRF Tokens Scanner) checks for missing tokens on POST endpoints. XSRFProbe CLI performs deep CSRF token analysis including omission, blank, random, and cross-user reuse testing. Semgrep rules python.django.security.audit.django-no-csrf-token and equivalent framework-specific rules catch missing @csrf_exempt decorators and missing token middleware in static analysis.
BreachVex detects CSRF via a segment-aware skip matcher that prevents false positives on read-only validation endpoints (/check, /validate, /preview) while running differential cross-origin probes on all state-changing paths. The scanner implements bearer/API-key immunity gating — endpoints using header-based auth are correctly identified as immune. The SameSite=Lax GET probe uses a cookie-jar clearing differential: GET with auth vs. GET without cookies — confirmed HIGH when the authenticated request succeeds while the cookieless request is rejected with 401/403.
Fetch Metadata headers (Sec-Fetch-Site, Sec-Fetch-Mode, Sec-Fetch-Dest) are attached by all modern browsers (98%+ coverage, 2024-2025) and allow server-side resource isolation policy enforcement. Reject any cross-site non-navigation request before it reaches application logic: if Sec-Fetch-Site == "cross-site" and method not in SAFE_METHODS: reject 403. This defense requires zero token management and blocks CSRF transparently.
The server generates a cryptographically random, session-bound token (minimum 128 bits via CSPRNG) and embeds it in every state-changing form. The server validates the token on submission — the Same-Origin Policy prevents the attacker from reading the token cross-origin.
# FastAPI — token generation and validation
import secrets
from fastapi import Request, HTTPException, Form
def generate_csrf_token(session: dict) -> str:
token = secrets.token_urlsafe(32) # 256-bit entropy
session["csrf_token"] = token
return token
async def validate_csrf(request: Request, csrf_token: str = Form(...)):
session_token = request.session.get("csrf_token")
if not session_token or not secrets.compare_digest(csrf_token, session_token):
raise HTTPException(status_code=403, detail="CSRF token invalid")Framework-specific implementations:
| Framework | CSRF Token Name | Mechanism |
|---|---|---|
| Django | csrfmiddlewaretoken | CsrfViewMiddleware — on by default; never use @csrf_exempt |
| Rails | authenticity_token | protect_from_forgery — on by default; API mode excludes it |
| Laravel | _token | @csrf Blade directive; VerifyCsrfToken middleware |
| ASP.NET | __RequestVerificationToken | [ValidateAntiForgeryToken] attribute |
| Spring Security | _csrf | CookieCsrfTokenRepository.withHttpOnlyFalse() |
| Express | _csrf | csrf-csrf package (csurf deprecated since 2022) |
| Angular | XSRF-TOKEN cookie / X-XSRF-TOKEN header | HttpClient reads and sends automatically |
// Express.js — csrf-csrf (modern replacement for deprecated csurf)
import { doubleCsrf } from "csrf-csrf";
const { generateToken, doubleCsrfProtection } = doubleCsrf({
getSecret: () => process.env.CSRF_SECRET,
cookieName: "__Host-psifi.x-csrf-token",
cookieOptions: { sameSite: "strict", secure: true, httpOnly: true },
});
app.get("/form", (req, res) => {
res.render("form", { csrfToken: generateToken(req, res) });
});
// Apply middleware — rejects requests without valid token
app.post("/account/email", doubleCsrfProtection, requireAuth, async (req, res) => {
await db.updateEmail(req.user.id, req.body.email);
res.json({ success: true });
});For stateless microservices and SPAs where storing tokens in server-side session is impractical, sign the token with HMAC-SHA256 bound to the session identifier and a server secret. The naive double-submit (random value in cookie + parameter, no HMAC) is vulnerable to cookie injection from compromised subdomains.
import hmac, hashlib, secrets, base64
def make_csrf_token(session_id: str, server_secret: bytes) -> str:
nonce = secrets.token_bytes(16)
sig = hmac.new(server_secret,
msg=(session_id.encode() + nonce),
digestmod=hashlib.sha256).digest()
return base64.urlsafe_b64encode(sig + nonce).decode()
def verify_csrf_token(token: str, session_id: str, server_secret: bytes) -> bool:
raw = base64.urlsafe_b64decode(token.encode())
sig, nonce = raw[:32], raw[32:]
expected = hmac.new(server_secret,
msg=(session_id.encode() + nonce),
digestmod=hashlib.sha256).digest()
return hmac.compare_digest(sig, expected)Set the cookie with __Host- prefix: Set-Cookie: __Host-csrf=<token>; Secure; Path=/; SameSite=Strict
The __Host- prefix prevents subdomain-injected cookies from overriding the legitimate CSRF cookie — blocking the sibling-domain double-submit bypass.
Always set an explicit SameSite attribute. Never omit it — omission triggers the Chrome 120-second Lax+POST grace window.
Set-Cookie: __Host-SID=<token>; Path=/; Secure; HttpOnly; SameSite=StrictSameSite=Strict is the correct default for admin panels, banking, and healthcare applications. It may break OAuth/SSO login flows where the authorization server redirect arrives cross-site — if so, use SameSite=Lax with an explicit CSRF token as a second layer.
Any custom header — X-Requested-With: XMLHttpRequest, X-CSRF-Token: <value> — forces a CORS preflight for cross-origin requests. Browsers cannot attach custom headers to simple cross-origin requests. Server-side: validate the custom header is present on all state-changing AJAX calls.
# FastAPI Fetch Metadata resource isolation policy
from fastapi import Request, HTTPException
SAFE_METHODS = {"GET", "HEAD", "OPTIONS"}
ALLOWED_SITES = {"same-origin", "same-site", "none"}
async def resource_isolation_policy(request: Request):
site = request.headers.get("Sec-Fetch-Site", "")
mode = request.headers.get("Sec-Fetch-Mode", "")
dest = request.headers.get("Sec-Fetch-Dest", "")
if request.method in SAFE_METHODS or site in ALLOWED_SITES:
return
if mode == "navigate" and dest not in ("object", "embed"):
return # allow top-level browser navigation
raise HTTPException(status_code=403, detail="Cross-site request rejected")Logout CSRF and account deletion via GET are real-world attack vectors. State-changing GET requests violate RFC 9110 §9.2 (safe methods MUST NOT modify state) but remain common in legacy code and admin panels. An attacker embeds <a href="https://target.com/logout"> or uses window.location to trigger a top-level GET — SameSite=Lax cookies are sent. Result: session termination (DoS) or data deletion. Every state-changing endpoint must require POST (or PUT/PATCH/DELETE) — never GET.
The BREACH attack (Browser Reconnaissance and Exfiltration via Adaptive Compression of Hypertext) applies CRIME-like compression oracle techniques to HTTPS responses. When a server compresses HTTP responses with gzip or Brotli and a secret value (such as a CSRF token) is reflected alongside attacker-controlled input in the same response body, an attacker can iteratively determine the token's value by measuring ciphertext length changes — exploiting Huffman encoding's property that matching byte sequences compress more efficiently. Each guess that shares bytes with the secret produces a slightly shorter ciphertext, leaking the token one character at a time via ~3,000–4,000 adaptive requests.
The mitigations are independent and complementary: (1) per-request token masking — XOR the session CSRF token with a random nonce on each response and transmit masked_token = token XOR nonce plus nonce; the server recovers token = masked_token XOR nonce — this makes every reflected value unique, destroying the oracle; (2) use the csrf-csrf library (Node.js) which implements this masking pattern by default; (3) disable HTTP compression (Content-Encoding: identity) on any response that reflects a secret alongside user-controlled input; (4) combine with Content-Security-Policy to prevent BREACH-assisted cross-origin reads via sub-resource injection.
Secondary defense for environments where token management is not possible. Validate Origin (preferred) or Referer (fallback) on all state-changing requests. Use exact matching only — a startsWith check allows https://target.com.evil.com to pass.
def validate_origin(request: Request, expected_origin: str) -> None:
origin = request.headers.get("Origin") or ""
referer = request.headers.get("Referer") or ""
source = origin or referer
# Exact match, then slash — prevents target.com.evil.com bypass
if not (source == expected_origin or source.startswith(expected_origin + "/")):
raise HTTPException(status_code=403, detail="Invalid origin")
# Explicitly reject null origin (sandboxed iframes)
if origin == "null":
raise HTTPException(status_code=403, detail="Null origin rejected")What is a CSRF attack? Cross-Site Request Forgery tricks an authenticated user's browser into sending a state-changing request to a trusted application without the user's knowledge. The browser automatically attaches session cookies to every request — the attacker's page exploits this to forge privileged actions. Classified as CWE-352.
What is the difference between CSRF and XSS? XSS injects script that executes in the victim's browser origin and can read data, steal cookies, and call APIs. CSRF forges outbound requests without reading responses — it executes actions. XSS can bypass CSRF tokens by reading them from the DOM via XMLHttpRequest.
Does SameSite=Lax prevent CSRF? Partially. It blocks cross-site POST form submissions but allows cross-site GET navigations and the Chrome 120-second Lax+POST grace window. Combine SameSite=Lax with a CSRF token.
What is the Synchronizer Token Pattern? A cryptographically random, session-bound token embedded in every state-changing form. The server validates it on submission. Attackers cannot read the token cross-origin, so forged forms fail.
Is a JSON API CSRF-immune?
No. enctype="text/plain" forms create a JSON-shaped body sent as Content-Type: text/plain — a CORS simple request that triggers no preflight. Servers that parse body JSON regardless of Content-Type are fully exploitable. CVE-2024-4994 (GitLab GraphQL, CVSS 8.1) demonstrates this.
What is Login CSRF? An attack that forces the victim to authenticate as the attacker's account on a pre-authentication endpoint (no session, no token). The victim then stores data in the attacker's account context.
What is the 2-minute Lax window? Chrome's Lax+POST grace period allows cross-site POSTs if the session cookie was issued within the past 120 seconds. An attacker forces re-authentication via a logout redirect then immediately fires the CSRF payload within that window.
Can CSRF bypass 2FA? No. CSRF operates on an existing authenticated session in which 2FA has already been passed. It cannot bypass the authentication step itself.
Cross-Site Request Forgery (CSRF) tricks an authenticated user's browser into sending a state-changing request to a trusted application without the user's knowledge. The browser automatically attaches session cookies to every request to the target domain — the attacker's page exploits this to forge privileged actions. Classified as CWE-352.
XSS injects and executes attacker-controlled scripts inside the victim's browser in the context of the target origin — it reads data and can steal cookies. CSRF forges outbound requests using the browser's automatic cookie attachment — it executes actions but cannot read responses. XSS can bypass CSRF tokens by reading them via XMLHttpRequest; combining both achieves account takeover.
CSRF is a client-side attack: the victim's browser is weaponized to send a request to a server the victim is authenticated with. SSRF is a server-side attack: the attacker tricks the server itself into making requests to internal or external infrastructure. CSRF requires user interaction; SSRF does not.
Partially. SameSite=Lax blocks cross-site POST form submissions and subresource requests but allows cross-site GET navigations. State-changing GET endpoints remain vulnerable. The Chrome 120-second Lax+POST grace window allows cross-site POSTs if the session cookie was issued within the past 2 minutes. SameSite=Lax must be combined with a CSRF token for complete protection.
Chrome applies SameSite=Lax behavior to cookies that omit the SameSite attribute, but grants a 120-second grace period after cookie issuance during which cross-site POST requests are allowed. An attacker who forces re-authentication (via a logout redirect) and then submits a CSRF payload within 2 minutes can bypass SameSite=Lax protection on newly-issued session cookies.
The Synchronizer Token Pattern (STP) generates a cryptographically random, session-bound token server-side and embeds it in every state-changing form as a hidden field. The server validates the token on submission — an attacker cannot read the token due to the Same-Origin Policy and therefore cannot forge a valid request. Tokens must be generated with a CSPRNG (minimum 128 bits) and must never appear in URLs.
The double-submit cookie pattern sets the CSRF token as both a cookie and a request parameter (or header). The server verifies they match. The Same-Origin Policy prevents cross-origin scripts from reading cookies, so an attacker cannot supply the matching value. The naive pattern (without HMAC signing) is vulnerable to cookie injection from a compromised subdomain — use HMAC-SHA256 binding to the session ID.
No. JSON APIs were assumed safe because application/json triggers a CORS preflight. However, an attacker can bypass this using a form with enctype=text/plain and a JSON-shaped body — the browser sends Content-Type: text/plain (a 'simple' type, no preflight required). If the server parses the body as JSON regardless of Content-Type, the attack succeeds. CVE-2024-4994 (GitLab GraphQL, CVSS 8.1) exploits exactly this pattern.
Login CSRF forces a victim to authenticate as the attacker's account before any session exists. Since pre-authentication state has no CSRF token to protect, the login form is typically unguarded. The victim then interacts with the application — entering payment data, uploading documents — all stored in the attacker's account. The attacker logs back in and harvests the victim's data.
The OAuth 2.0 state parameter (RFC 6749 §10.12) is a per-request CSRF token binding the authorization request to the user's session. If the state is absent or static, an attacker forges the callback URL with their own authorization code — linking the attacker's social account to the victim's application account (account takeover). RFC 9700 (OAuth 2.0 Security BCP, January 2025) mandates cryptographic state binding.
GraphQL CSRF exploits the fact that GraphQL accepts state-changing mutations via GET requests (no preflight), via application/x-www-form-urlencoded (no preflight), and sometimes via text/plain (no preflight). CVE-2024-4994 (GitLab, CVSS 8.1) and CVE-2025-68604 (WPGraphQL, CVSS 5.4) demonstrate real-world GraphQL CSRF. Apollo Server now requires Apollo-Require-Preflight header by default.
CSRF does not bypass 2FA directly — it operates on an existing authenticated session where 2FA has already been completed. If the victim is already logged in and has passed 2FA, CSRF can forge any action the victim is authorized to perform in that session. 2FA prevents unauthorized login but does not protect against CSRF on authenticated endpoints.
1) Identify all state-changing endpoints (POST/PUT/PATCH/DELETE plus any GET endpoints modifying data). 2) In Burp Suite, right-click any request → Engagement tools → Generate CSRF PoC. 3) Test token bypass: remove the token entirely, submit a blank token, submit a random token, submit another user's valid token. 4) Test Content-Type bypass: change application/json to text/plain. 5) Check GraphQL for GET mutations and form-encoded bodies. 6) Check OAuth flows for missing or static state parameters.
Modern browsers attach Sec-Fetch-Site, Sec-Fetch-Mode, and Sec-Fetch-Dest headers to every request. Server-side resource isolation policy uses Sec-Fetch-Site: cross-site to detect and reject forged cross-origin requests before they reach application logic. Coverage is 98%+ globally as of 2024-2025. For the remaining 2%, Origin/Referer header validation provides fallback.
CSRF CVSS varies by impact. Typical CSRF on a user-data endpoint: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N = 6.5 High. CSRF → password/email change (account takeover path): ~8.1 High. CSRF → admin account creation or critical config: ~9.0 Critical. CVE-2024-20252 (Cisco Expressway) scored 9.6 Critical for CSRF enabling system configuration changes at administrator privilege level.