State-changing GET request triggered by an img tag or link, exploiting endpoints that incorrectly accept GET for mutations.
TL;DR
<img>, <a href>, window.location, meta refresh — no user click required for <img> tagswindow.location and <a> exploits succeedClassic GET-based CSRF (CWE-352) exploits web endpoints that perform state-changing operations in response to GET requests. RFC 9110 §9.2 defines GET as a "safe" method that MUST NOT modify server state. When developers implement destructive or state-changing functionality on GET endpoints — legacy PHP admin panels, quick-action buttons, logout links — they expose every authenticated visitor to one-click attack via any page they visit.
The attack requires no user interaction beyond visiting the attacker's page. An <img> tag with a src pointing to the target endpoint causes the browser to issue an authenticated GET request immediately upon page load. The victim never sees anything visual — a zero-pixel image is invisible. This makes GET-based CSRF the simplest, most reliable CSRF variant: no JavaScript required, no form submission needed, and no SameSite=Lax protection for top-level GET navigation attacks.
The modern distinction matters: <img> triggers a subresource GET (blocked by SameSite=Lax), while <a href> or window.location triggers a top-level navigation GET (Lax cookies ARE sent). Both vectors work against different cookie configurations. Against SameSite=None or absent cookies, both work. Against SameSite=Lax, only the navigation variants succeed.
The attack chain uses the browser's automatic cookie attachment on any outbound request to a domain for which the victim holds a valid session.
Attack steps:
target.com (session cookie present in browser).evil.com — either via phishing, malvertising, or a compromised page.<img> tag, script redirect, or clickable link targeting a state-changing GET endpoint.Attack payloads — three delivery vectors:
<!-- Vector 1: Image tag — zero-click, fires on page load -->
<!-- Works against SameSite=None/Absent; BLOCKED by SameSite=Lax (subresource) -->
<img src="https://bank.com/transfer?to=attacker&amount=5000" width="1" height="1">
<!-- Vector 2: Top-level navigation redirect — bypasses SameSite=Lax -->
<!-- Lax cookies ARE sent on top-level GET navigations -->
<script>window.location = 'https://bank.com/transfer?to=attacker&amount=5000';</script>
<!-- Vector 3: Clickable link — social engineering, bypasses SameSite=Lax -->
<a href="https://target.com/admin/users/42/delete">Click here to claim your prize</a>The critical SameSite=Lax distinction:
Browser sends SameSite=Lax cookie on: top-level GET navigation (window.location, <a href>, address bar)
Browser BLOCKS SameSite=Lax cookie on: <img src>, <script src>, <iframe src>, fetch(), XMLHttpRequestAttackers targeting Lax-protected apps use window.location or redirect chains rather than <img> tags. The practical difference is that the navigation vector requires a page visit trigger (easily achieved with an auto-redirect) rather than page load.
| Variant | Technique | SameSite=Lax Blocked? | Impact |
|---|---|---|---|
Image tag <img src> | Zero-click, fires on page load | Yes (subresource) | Account action on SameSite=None |
Script redirect window.location | Zero-click via JS on page load | No (top-level nav) | Account action despite Lax |
Anchor link <a href> | One-click social engineering | No (top-level nav) | Account action despite Lax |
Meta refresh <meta http-equiv="refresh"> | Zero-click, no JS required | No (top-level nav) | CSRF in strict CSP environments |
Method override ?_method=POST | GET treated as POST by framework | No (top-level GET) | POST-endpoint CSRF via Lax |
| Cached CSRF | Attacker caches state-change URL in CDN | No | Persistent CSRF trigger |
The method override vector deserves particular attention. Symfony, Rails (_method), Laravel, and some Express middleware accept a _method=POST query parameter on GET requests. An attacker sends a GET with ?_method=POST — the browser sends SameSite=Lax cookies because it's a GET — and the framework processes it as a POST, executing the state change.
GET /account/email?_method=POST&email=attacker@evil.com HTTP/1.1
Host: target.com
Cookie: session=victim_session_tokenCVE-2024-23902 — Jenkins GitLab Branch Source Plugin: Form validation on this plugin endpoint did not require POST — the endpoint accepted GET requests for state-changing operations including connecting to attacker-specified URLs. CVSS Medium, vector AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N. Fixed in version 688.v5fa_356ee8520. Published January 24, 2024.
CVE-2024-20254 — Cisco Expressway CSRF (CVSS 9.6): The Cisco Expressway API management interface contained insufficient CSRF protections on endpoints controlling device configuration. An unauthenticated remote attacker could trick an authenticated administrator into visiting a crafted link, triggering arbitrary configuration changes at admin privilege level. CVE-2024-20254 was exploitable in the default Expressway configuration, with no authentication precondition for the attacker.
CVE-2023-47640 — Grails Framework (CVSS 8.8): Grails' built-in CSRF protection was bypassed by completely omitting the token field from GET requests to endpoints that the framework should have protected. The token omission pattern — accepted when missing — is the foundation of both GET and POST CSRF attacks. Affected Grails < 5.3.4, requiring a framework upgrade.
HackerOne #2326194 — Argo CD / Kubernetes ($4,660): Insufficient request validation in Argo CD's GitOps management interface allowed CSRF to affect the entire Kubernetes cluster configuration — arbitrary cluster reconfiguration via CSRF on a management endpoint.
delete, remove, destroy, transfer, update, promote, ban, revoke, approve, confirm, reset. GET endpoints containing these words are high-priority targets.Set-Cookie headers for SameSite attribute. Absent or SameSite=None means <img> attacks work. SameSite=Lax means navigation vectors work. SameSite=Strict blocks most GET vectors (test open redirect chain for bypass).BreachVex detects GET-based CSRF using a two-probe differential: authenticated GET (records status) vs. unauthenticated GET (cookie jar cleared). Endpoints returning HTTP 2xx with auth and HTTP 401/403 without auth are confirmed auth-gated. The probe skips irreversible destructive endpoints in safe mode (paths containing delete, destroy, kill) to prevent side effects during scanning.
Never perform state changes on GET requests. Follow RFC 9110 §9.2: GET, HEAD, and OPTIONS must be idempotent and side-effect-free.
# FastAPI — enforce POST for all state-changing operations
from fastapi import APIRouter, Request, HTTPException, Depends
router = APIRouter()
# VULNERABLE: state change on GET
@router.get("/admin/users/{user_id}/delete")
async def delete_user_bad(user_id: int):
await db.delete_user(user_id) # DO NOT DO THIS
return {"ok": True}
# SAFE: state change on POST with CSRF token validation
@router.post("/admin/users/{user_id}/delete")
async def delete_user_safe(
user_id: int,
request: Request,
csrf_token: str = Form(...),
current_user = Depends(require_admin)
):
validate_csrf_token(request, csrf_token)
await db.delete_user(user_id)
return {"ok": True}<!-- Every state-changing form must include the CSRF token -->
<form action="/admin/users/42/delete" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit">Delete User</button>
</form>// For AJAX requests: read token from meta tag and include in headers
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
fetch('/admin/users/42/delete', {
method: 'POST',
headers: { 'X-CSRF-Token': csrfToken, 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ confirm: true }),
});Set-Cookie: __Host-SID=<token>; Path=/; Secure; HttpOnly; SameSite=StrictUse SameSite=Strict rather than Lax to block all cross-site requests including top-level navigation. The __Host- prefix prevents subdomains from injecting override cookies.
SameSite=Lax does not protect state-changing GET endpoints. When the browser follows a link or redirect to a GET endpoint, Lax cookies are included. If that GET endpoint modifies data, the attack succeeds. The only defense against top-level navigation GET CSRF is to not accept state changes on GET at all.
Yes. GET-based CSRF occurs when an application performs a state-changing action in response to a GET request, violating RFC 9110 §9.2. An attacker embeds an <img>, <a>, or <script> tag pointing to the state-changing URL. The browser automatically sends the victim's session cookies with the request.
Partially. SameSite=Lax blocks GET requests from subresource tags (<img>, <script>) since those are not top-level navigations. However, SameSite=Lax explicitly sends cookies on top-level GET navigations—meaning an attacker using window.location redirect or a clickable link can still trigger GET-based CSRF.
A top-level navigation changes the URL in the browser's address bar. This includes clicking a link, window.location assignments, meta refresh, and form submissions that navigate the main frame. Subresource requests (img, script, iframe, fetch) are not top-level navigations and are blocked by SameSite=Lax.
RFC 9110 §9.2 defines safe HTTP methods. Safe methods MUST NOT modify server state—they must be purely informational. GET, HEAD, and OPTIONS are safe by this definition. Any server that changes state on a GET request violates RFC 9110 and creates a GET-based CSRF surface.
Proxy all traffic through Burp Suite and filter for GET requests that return 2xx. Look for path segments containing: delete, remove, destroy, transfer, update, promote, ban, revoke, approve, confirm. Test each by replaying the request without a session cookie—if it returns 401/403, the endpoint is auth-gated and worth testing for CSRF.
Logout CSRF forces the victim to log out by triggering the /logout endpoint cross-site. Impact is denial of service (session loss) rather than ATO. It's rated informational to low severity. Unlike account-action CSRF, logout CSRF cannot be combined with data theft unless paired with a session fixation attack.
Admin panel quick-action links are the highest-impact GET CSRF scenario. If a CMS, forum, or SaaS admin panel uses GET for user deletion, role promotion, or content publishing, an attacker who phishes an admin gets full admin-level CSRF. CVE-2024-20254 (Cisco Expressway, CVSS 9.6) exploited an admin action trigger without user authentication.
BreachVex uses a differential probe: it sends the GET request with authentication headers, records the status code, then clears the cookie jar and re-sends without cookies. If the authenticated request returns 2xx and the unauthenticated request returns 401/403, the endpoint is auth-gated and state-changing—confirmed GET CSRF.