Cross-Site Request Forgery (CSRF)
Cross-site request forgery (CSRF) is an attack that makes a logged-in user's browser send a state-changing request the user never intended. Because the browser attaches the session cookie automatically, an application that trusts the cookie alone cannot tell the forged request from a real one. Example: while a user is signed in to a store, a page on another site silently submits a form to the store's change-e-mail URL; if the store checks only the cookie, the e-mail address changes.
CSRF matters only for actions that change state and rely on credentials the browser sends automatically, such as session cookies and HTTP Basic auth. Every defence and every test in this lesson turns on one idea: proof that the request came from the application's own pages, not just proof that the user is logged in.
Why it matters
A successful CSRF changes data or runs an action as the victim: a changed e-mail or password can lead to account takeover, and money or settings can move without consent. The user did nothing wrong and sees nothing, which makes it hard to detect after the fact.
These defects pass every functional test, because the normal journey works. They surface only in tests written for them, and those tests are cheap: a captured request, a test account and a tool to replay it. Catching a missing or unchecked token before release closes a route to account takeover that users cannot protect themselves against.
How a forged request works
A CSRF needs three things to line up. First, a relevant action that changes state, such as changing an e-mail, transferring money or deleting data. Second, session handling that rides on credentials the browser sends by itself, so the attacker's page does not need to know them. Third, request parameters the attacker can predict: if every value in the request is guessable, the page can build it in advance.
The attacker does not break into the session and never sees the response. A page the victim visits — an advert, a forum post, an e-mail with remote content — tells the victim's browser to issue the request to the target application. The browser, following its normal rules, attaches the cookies for that site. The server receives a well-formed, authenticated request and, unless it demands more than the cookie, carries it out. This is why a page on another site, which cannot read anything from the target, can still cause an action there.
- A hidden form that auto-submits a
POSTis the classic carrier; an<img>or a link can carry aGET. - The attacker needs no XSS and no script on the target: CSRF rides the browser's own behaviour, which is what makes it different from the cross-site scripting in the previous lesson.
- Because the attacker cannot read the response, CSRF is about forcing an action, not stealing data — that distinction shapes every test.
Only the cookie is checked
- Victim’s browser signed in to app.example
- Page on another site
attacker.example - Target application
app.example
- 1 From Victim’s browser To Target application: Sign in; the reply sets the session cookie
Set-Cookie: sid=9f8a… - 2 From Victim’s browser To Page on another site: Open a page on another site: an advert, a forum post
- 3 From Page on another site To Victim’s browser: Hidden form that submits itself to app.example reply
POST /account/email - 4 From Victim’s browser To Target application: Forged request; the browser attaches the cookie by itself
Cookie: sid=9f8a… - 5 From Target application To Victim’s browser: Only the cookie is checked: the e-mail changes reply The attacker never sees this reply and does not need to
200 OKFail
Outcome: The cookie proved who; nothing proved where the request came from. Fail
Token checked on the server
- Victim’s browser signed in to app.example
- Page on another site
attacker.example - Target application
app.example
- 1 From Victim’s browser To Target application: Sign in; the cookie is set and the app’s own forms carry a session-bound token
sid=9f8a… token=7bQ2… - 2 From Victim’s browser To Page on another site: Open a page on another site: an advert, a forum post
- 3 From Page on another site To Victim’s browser: Hidden form without the token: another site cannot read it reply
POST /account/email - 4 From Victim’s browser To Target application: Forged request; the browser attaches the cookie by itself
Cookie: sid=9f8a… - 5 From Target application To Victim’s browser: Token missing: rejected before anything changes reply
SameSiteon the cookie and anOrigincheck are two further layers403 ForbiddenPass
Outcome: The cookie proves who the user is; the token proves the request came from the application’s own page. Pass
The three defences: tokens, SameSite and Origin
The classic primary defence is an anti-CSRF token: an unpredictable value the server binds to the user's session and places in each page's forms. A genuine form sends it back; a cross-site page cannot read it, so it cannot include it. The synchronizer token pattern keeps the expected value server-side; the double-submit pattern sends it in both a cookie and the request and compares them, which needs no server state but should use a value the server signs and ties to the session, so an attacker who manages to plant a cookie still cannot make a matching pair. Either way the server must reject the request when the token is missing or wrong — a token that is present but never checked defends nothing.
SameSite on the session cookie is the second layer. Strict or Lax stops the browser from attaching the cookie to most requests that start on another site, which blocks the plain forged form. Treat it as defence in depth, not a replacement for tokens: Lax still sends the cookie on top-level GET navigations from another site, and defaults differ between browsers, which is why the lesson on cookies and sessions over HTTP expects the attribute to be set explicitly.
The third layer is the Origin header, with Referer as a fallback: on a state-changing request the server confirms the origin is its own. Browsers add Origin to cross-origin requests and a page cannot change it, so a mismatch is a strong reason to reject; the header can still be missing or null, so the server must also decide what happens then. The OWASP Cross-Site Request Forgery Prevention Cheat Sheet lists this check, like SameSite, as defence in depth. Current browsers also send Sec-Fetch-Site (for example cross-site), which OWASP accepts as a main check for modern browsers when Origin serves as the fallback. None of this is CORS, which governs what a page may read: a plain cross-site form post reaches the server whatever the CORS headers say, which is why the lesson on CORS said CORS is not a CSRF defence.
Worked example: testing a change-e-mail action
Take a sandbox application you own or have written permission to test. Sign in with a test account and capture the request that changes the account e-mail, with an intercepting proxy as in the lesson on intercepting proxies, or from the browser's developer tools. The captured request below carries a session cookie and a token.
Now replay it with one thing changed at a time, using the proxy's replay feature or by rebuilding it in the REST API Tester or as a cURL command, and watch the response and the stored e-mail. Each replay is a test case with a clear expected result. A replay tests what the server checks; it cannot show what a browser would send, which is why SameSite is checked separately on the Set-Cookie header.
- Remove
X-CSRF-Tokenand replay: expect a rejection (for example403) and the e-mail unchanged. If it succeeds, the action has no token check. - Replace the token with a valid one from a second test account's session and replay: expect rejection. If it succeeds, the token is not tied to the session.
- Keep the valid token, change
Originto another site and replay: a rejection shows the server checks the origin as well; acceptance means the token is the only layer, which you record as an observation, not a failure. - Paste the login
Set-Cookieinto the Cookie Analyzer and confirm the session cookie setsSameSiteexplicitly. - Look for any action reachable by
GETthat changes state: that is a defect on its own, because evenSameSite=Laxsends the cookie on a followed link.
POST /account/email HTTP/1.1
Host: app.example
Origin: https://app.example
Cookie: sid=9f8aUq0Zr8Ty2Nc6
X-CSRF-Token: 7bQ2mK9rLp4XwZ1
Content-Type: application/x-www-form-urlencoded
email=sara%40app.exampleTry it: REST API Tester Try it: cURL Generator Try it: Request Header Inspector
A repeatable CSRF test for every release
Scale the worked example into a checklist. List every state-changing request the feature makes; for each, record whether a token is required, whether it is unpredictable and session-bound, whether the server checks it, and whether Origin or Referer are validated. The token must travel in the body or a header, never the query string, where it leaks through the Referer header, server logs and browser history.
Watch for checks that run only in the page's own JavaScript, which a forged request never passes through, tokens that are reflected but never verified, and GET endpoints that quietly change state. RFC 9110 defines GET as a safe method, one whose semantics are essentially read-only, so a state change behind GET is a defect even before CSRF is considered. Record the exact request, what you changed and the response, so a developer can reproduce the finding in one step.
Common mistakes
- Testing only that a valid request works and never replaying one with the token removed or altered.
- Treating a present token as a passing result — the test is whether the server rejects a request with a missing or wrong token.
- Trusting
SameSitealone, and forgetting thatLaxstill sends the cookie on top-levelGETnavigations from another site. - Confusing CSRF with XSS or CORS: XSS runs script in the site's own origin and can read the token, and CORS only governs what a page may read, not whether an action runs.
- Putting the CSRF token in the URL, where it leaks through the
Refererheader, server logs and browser history. - Leaving state-changing actions on
GETrequests, which no token discipline on forms will protect.
The QA perspective
On a team, CSRF is a review question before it is a test. When a new form or endpoint appears, the tester asks how it is protected — token, SameSite, origin check — and agrees the expected rejection with the developer, so the test has a clear oracle. The answers belong in the test case, not in memory.
Testing is always against a system you own or have written permission to test, with a test account and non-destructive values; you are confirming that a defence exists and is enforced, not attacking anyone. A finding names the request, the single change that was accepted and the response; an account-changing action without a working token check is usually rated high severity, because it can lead to account takeover.
Practice exercise
Decide which requests are exposed to CSRF
Below are four requests from a banking sandbox where the user is logged in with a session cookie. For each, decide whether it is exposed to CSRF as shown, name the missing defence, and write the expected result of replaying it from another origin.
Assume the session cookie is sent automatically and that no Origin check is in place unless a request shows one.
1 GET /transfer?to=ACME&amount=500 HTTP/1.1
Cookie: sid=Zc4Hn8Tq1Wy6Bd3F
2 POST /transfer HTTP/1.1
Cookie: sid=Zc4Hn8Tq1Wy6Bd3F
Content-Type: application/x-www-form-urlencoded
to=ACME&amount=500
3 POST /transfer HTTP/1.1
Cookie: sid=Zc4Hn8Tq1Wy6Bd3F
X-CSRF-Token: Lm8Vb3Nk7Xr5Tq2
to=ACME&amount=500
4 POST /profile/theme HTTP/1.1
Cookie: sid=Zc4Hn8Tq1Wy6Bd3F
theme=dark- For each request, is the action state-changing?
- Does it carry an unpredictable, session-bound token the server would check?
- Could another site cause this request with the cookie attached?
- State the expected result once the defence is in place.
Show the model answer
Request 1 is the worst: it changes state through a GET, so a link or an <img> on any site triggers it, and even SameSite=Lax sends the cookie on a followed link. Move it to POST and add a token; expected result after the fix: rejected from another origin.
Request 2 is state-changing over POST but carries no token, so a hidden auto-submitting form on another site succeeds. Add a session-bound token the server checks; expected result: 403 without a valid token.
Request 3 carries a token in a header. If the server verifies it against the session it is protected, because a cross-site page cannot read the token to include it. The test is to replay it with the token removed, and again with a token from another session, and confirm both are rejected.
Request 4 changes a low-risk setting but is still CSRF-exposed exactly like request 2; the theme can be flipped for any logged-in user who opens the attacker's page. Severity is lower, not absent. Protect it the same way, and note that an action's low impact is about severity, not about whether the defence is needed.
Frequently asked questions
Is CSRF still relevant now that some browsers default cookies to SameSite=Lax?
Yes. Defaults differ between browsers and versions, Lax still allows top-level GET navigations, and some applications set SameSite=None for legitimate cross-site needs. The OWASP cheat sheet still asks for a server-side check, normally a token, as the main defence and treats SameSite as defence in depth.
What is the difference between CSRF and XSS?
XSS runs the attacker's script inside the site's own origin, so it can read the page, the token and the response; CSRF runs no script on the target and cannot read the response — it only forces an action. An XSS flaw can defeat CSRF tokens, which is why both are tested.
Do REST APIs that use a bearer token in a header need CSRF protection?
If the API authenticates only by a token the client must add to each request — not by a cookie the browser sends automatically — it is not exposed to classic CSRF, because the browser never adds that token by itself and a page on another site cannot read it. An API that accepts a session cookie is exposed and needs the same defences.
Can I test CSRF on any site to learn?
No. Test only systems you own or have written permission to test, with a test account and non-destructive values. Forging requests against someone else's application is an attack, regardless of intent.
References
- OWASP Cheat Sheet Series: Cross-Site Request Forgery PreventionCSRF defences: synchronizer tokens, SameSite cookies and origin checks.
- OWASP Web Security Testing Guide (WSTG)The reference methodology for testing web applications and APIs for security weaknesses, test by test.
- Using HTTP cookies — MDNCookie lifetime, scope, the Secure/HttpOnly/SameSite attributes and their security implications.
- RFC 9110: HTTP SemanticsThe meaning of HTTP methods, header fields, status codes and content negotiation, independent of the wire version.
Practise with Mutqan tools
Parse Set-Cookie headers and check Secure, HttpOnly, SameSite, Domain, Path, expiry and size best practices.
Inspect the request headers your browser or client sends, with explanations for each.
Parse raw HTTP request/response headers into a structured table with explanations.
Test REST endpoints with full control over method, headers and JSON body, then assert on status, headers and JSON paths.
Build cURL commands from method, URL, headers, body and auth options.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.