Tooling
Burp's CSRF PoC generator and where it produces a 2026-invalid PoC, ZAP, why curl proves nothing, and browser devtools.
The tool that matters most is a second browser
CSRF tooling is thin, because the hard part is not generating a request — it is knowing whether a browser would send the cookie. No scanner answers that reliably, because the answer depends on the cookie's attributes, the delivery shape, and the browser engine, and most scanners model none of those.
So the practical toolkit is:
- A proxy (Burp or ZAP) to modify and replay requests — this is where the token-validation testing happens.
- Two browsers, Chrome and Firefox, to verify the PoC. This is the step that scanners cannot do and that decides what you can claim.
- A place to host the PoC on a genuinely different origin.
- This site's PoC Generator for building the HTML and getting the per-engine cookie verdict, and the Lab for seeing the mechanics.
What is not in the toolkit: curl. See below.
Burp Suite
Generate CSRF PoC — right-click a request in Proxy history, then Engagement tools → Generate CSRF PoC. It builds an auto-submitting HTML form from the request.
It is the fastest way to a first draft, and it has three limitations worth knowing:
- It says nothing about
SameSite. It will happily generate a cross-site POST form for a request whose cookie isSameSite=Strict, which cannot work. The generated PoC is syntactically fine and functionally dead. Always check the cookie yourself. - It cannot express what HTML cannot. For a
PUTor aDELETE, older versions emitted<form method="PUT">, which browsers silently downgrade toGET— so the PoC tests something other than the request you selected. Check the generatedmethodattribute; if the original was notGETorPOST, you need a method override or a scripted delivery. - JSON bodies need the text/plain treatment. Burp will produce a form with the JSON as a field value, which is not the same thing. Use the padding construction from JSON, text/plain, and GraphQL.
Other Burp features worth using: Repeater for the token-validation sequence, the WebSockets history for CSWSH, and Comparer to diff tokens across sessions when checking predictability.
Why curl proves nothing
# This "works" against almost any endpoint and demonstrates nothing at all.
curl -X POST https://app.example/account/email \
-H 'Cookie: session=eyJ1c2VyIjoidmljdGltIn0' \
-d 'email=attacker@evil.example'
# You supplied the cookie. You controlled every header. You sent no Origin,
# no Referer, no Sec-Fetch-Site. Nothing here resembles what a browser would
# do from an attacker's page.
#
# All this proves is that the endpoint exists and that the session is valid.
#
# It is still USEFUL -- as a fast way to test SERVER-SIDE checks:
# Does the server reject a cross-site Origin?
curl -X POST https://app.example/account/email \
-H 'Cookie: session=...' \
-H 'Origin: https://evil.example' \
-d 'email=x@example.com'
# Does it reject a cross-site Sec-Fetch-Site?
curl -X POST https://app.example/account/email \
-H 'Cookie: session=...' \
-H 'Sec-Fetch-Site: cross-site' \
-H 'Sec-Fetch-Mode: navigate' \
-d 'email=x@example.com'
# Does it parse a text/plain body as JSON?
curl -X POST https://api.example/account \
-H 'Cookie: session=...' \
-H 'Content-Type: text/plain' \
-d '{"email":"x@example.com"}'
# These are the right use of curl: probing what the SERVER checks. The
# question of what the BROWSER sends can only be answered in a browser.Hosting the PoC
The PoC must be served from a genuinely different site, or you are not testing what you think you are.
- A
file://page is not equivalent. It has an opaque origin, sendsOrigin: null, and is subject to different cookie handling. A PoC that works fromfile://may fail from a real origin, and vice versa. localhostagainst a remote target is cross-site and usable, but note that aSecurecookie will not be sent overhttp://localhostto a different host, and schemeful same-site applies.- A subdomain of the target is same-site, so hosting there tests the cookie-tossing scenario rather than the ordinary cross-site one. Useful deliberately, misleading accidentally.
- Match the scheme. An
http://PoC against anhttps://target is cross-site by scheme and will not send aSecurecookie.
For client work, the cleanest option is a scratch domain you own with a valid certificate. A cloud storage bucket or static host works; so does an ngrok-style tunnel to a local server, provided it terminates TLS.
And take the PoC out of the browser's cache between runs — a cached auto-submitting form can produce confusing results when you are changing it repeatedly.
Browser devtools
The most direct answers come from the browser itself:
- Application → Cookies shows every attribute at a glance:
SameSite,Secure,HttpOnly,Domain,Path, and whether the name carries a__Host-prefix. Faster than readingSet-Cookieheaders, and it shows the effective state after all responses. - Network → any request → Cookies tab shows which cookies were actually sent and, when one was blocked, why. Chrome flags blocked cookies with the reason — this is the single most useful view when a PoC does not fire, because it tells you whether the cookie was withheld or the request itself failed.
- The Issues panel in Chrome reports SameSite-related warnings and upcoming policy changes.
- Network → WS filters WebSocket connections and shows the handshake headers.
- Disable cache while iterating on a PoC.
When a PoC does not work, the question is always "did the request go out, and did it carry the cookie?" Both are visible here in about ten seconds, and guessing at it is what turns a five-minute test into an afternoon.
Other tools
Related
Enumerate, classify by auth mechanism, read the SameSite value first, then test — and write the finding with the browsers named.
Token not checked, checked only when present, empty accepted, not tied to the session, reused across users, or predictable.
Smuggling a JSON document through enctype="text/plain", content-type confusion in body parsers, and GraphQL over GET and form encodings.
GET and POST routing, _method and X-HTTP-Method-Override, and body parsers that ignore the declared content type.
The handshake is a subresource GET with no CORS — which is why it needs Origin validation, and why SameSite=None is its precondition.
What still works and what died with Lax-by-default — the honest, browser-by-browser exploitability verdict, plus the techniques that are now purely historical.