Skip to content
highCVSS 8.1CWE-352A01:2021 – Broken Access Control

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

HTMLVulnerable
<!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:

  1. 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.
  2. Use a popup. window.open is top-level, so it keeps the cookie, and the original page stays put. Requires a user gesture to survive popup blocking.
  3. 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 GET and POST to the same handler, match or any route 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 TRACE too; Rails exempts only GET and HEAD. If a route is reachable by an exempted method, the token is not checked. The full comparison is on the Cheatsheet.
  • SameSite=Strict does 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.