GET Delivery
img, script, link, scripted link clicks, and top-level navigation — and why only the last one still carries a cookie.
The most productive vector left
If the target performs a state change in response to a GET, you almost certainly have a working attack — in every browser, against SameSite=None, SameSite=Lax, and an absent attribute alike. Only SameSite=Strict blocks it.
That is because Lax was designed to keep ordinary web browsing working. Clicking a link from a search engine to a site you are logged into has to keep you logged in, so Lax sends the cookie on a top-level navigation with a safe method. GET is a safe method. The browser has no way to know that this particular GET transfers money.
So the modern rule is blunt: SameSite protects you from cross-site POST. It does not protect you from cross-site GET. An application that changes state on GET has gained nothing from the last six years of cookie hardening.
The critical distinction inside this guide is between a navigation and a subresource. They have completely different verdicts now, and most write-ups still treat them as interchangeable.
Top-level navigation
<!doctype html>
<html>
<body>
<p>Loading your document...</p>
<script>
// The whole attack. A top-level navigation with a safe method, so the
// cookie rides along under None, Lax, and unset alike.
location = 'https://bank.example/transfer?to=attacker&amount=5000'
</script>
</body>
</html>The visibility problem
A top-level navigation is the shape that works, and it is also the shape the victim can see: their tab lands on the target's page. For a realistic attack chain that matters, and there are three standard answers:
- Navigate and come back. Send the victim to the target, then have the target's response redirect onward — or simply accept a moment of visibility on a page that looks innocuous, like a logged-in dashboard.
- Use a popup.
window.openis top-level, so it keeps the cookie, and the original page stays put. Requires a user gesture to survive popup blocking. - Do it on the way out. Trigger the navigation as the victim leaves — from a link they were already clicking. The target page loads in the tab they have abandoned.
What no longer works is the classic "invisible <img> in the corner". That was the standard trick precisely because it was silent, and it is dead against cookie auth in all three engines. It remains live against Basic auth and against local-network targets — see below.
Where subresource GET is still live
<img> and friends are dead against cookies, but the shape is not useless. Three cases keep it relevant:
Non-cookie ambient credentials. Basic, Digest, NTLM, Negotiate, and TLS client certificates are not cookies, so no cookie policy touches them. An <img> pointed at an intranet host behind Windows Integrated Auth authenticates in 2026 exactly as it did in 2006. See Routers, IoT, and localhost.
SameSite=None; Secure in Chromium. Applications that opted out for embedding or third-party API reasons are back to the old rules — in Chromium only. Firefox partitions and Safari blocks, so this is a Chromium-only finding and must be reported as one.
Same-site contexts. From a subdomain you control, nothing is cross-site and every shape works. See Subdomain Cookie Injection.
One useful property survives everywhere: an <img> produces no visible navigation and no console noise, so it is the quietest way to probe whether an endpoint exists, even when the cookie will not be attached.
Prevention
The fix is not a better token — it is not doing state changes on GET.
- Make GET, HEAD, and OPTIONS side-effect free. This is what "safe method" means in the HTTP spec, and every CSRF framework in existence exempts those methods from token checking on the assumption that you honoured it. If you change state on GET, you have opted out of your framework's protection without realising.
- Watch for accidental GET routes. Frameworks that route
GETandPOSTto the same handler,matchoranyroute declarations, and "convenience" endpoints like/logout,/delete?id=,/admin/promote?user=are the usual offenders. - Check the safe-method list your framework uses. Django and ASP.NET Core exempt
TRACEtoo; Rails exempts onlyGETandHEAD. If a route is reachable by an exempted method, the token is not checked. The full comparison is on the Cheatsheet. SameSite=Strictdoes block this, but it also logs users out when they arrive from an external link, which is why almost nobody uses it for session cookies. Treat it as available for a second, high-value cookie rather than the main session — see Defense in Depth.
Related
Exactly when Lax, Strict, None, and an absent attribute attach a cookie to a cross-site request — and why the three engines disagree.
The urlencoded and multipart form: the only primitive that issues a cross-site request with a body, ambient credentials, and no preflight.
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.
Top-level form navigation reaches private addresses that Local Network Access does not gate, and Basic/NTLM/client-cert auth is ambient and SameSite-immune.
Lax top-level GET, Chromium's 120-second Lax+POST window, on-site redirect gadgets against Strict, and schemeful downgrade.
Making the victim submit the real form with the real token — and why Lax-by-default has quietly killed most framed sessions.
The layering order — SameSite=Lax, __Host- prefix, Fetch Metadata rejection, then a token — plus re-authentication for high-value operations.