CSRF (Cross-Side-Request-Forgery) vulnerabilities are with us since the beginning of the web. However, things are highly changed since then. New web technologies, stacks, communication methods etc. are developed. Also, browsers are integrating built-in mechanisms to protect users from vulnerabilities by default. CSRF vulnerabilities are affected by that as well. However, most of the tutorials are still using CSRF explanations from 10 years ago and most of the techniques are not relevant today. Because of that, most people don’t know the cases where CSRF would work or not.
Let’s take the most basic example that is shown in all tutorials:
<html>
<body>
<form action="http://bank.com/transfer" method="post">
<input type="hidden" name="Transfer" value="myaccount" />
<input type="hidden" name="Amount" value="3000000" />
<input type="submit" value="Click"/>
</form>
</body>
</html>
The “bank.com/transfer” endpoint does a transfer action with the “Transfer” and “Amount” parameters that came from a POST request. Since this endpoint doesn’t require a random CSRF token, an attacker can put this HTML code to the attacker.com, sends the link to the victim, when the victim opens the link, the transfer happens. Right?
Well, not exactly.
There are different cases that it can happen or not. For example, if the victim uses Chrome browser, it won’t work. If the victim uses Safari, it would work. So, can we say that CSRF doesn’t happen in Chrome anymore? No. If the victim logged in to bank.com 2 minutes ago, it will work. If 2 minutes have passed, it won’t work.
If the “bank.com/transfer” accepts content-type as “text/html” it would work, if it only accepts “application/json” it won’t work.
I know it may confusing. To understand these cases, we need to understand two concepts: SameSite Cookies and Same Origin Policy. Let’s go step by step.
SameSite Cookies
SameSite is a cookie attribute just like HttpOnly and Secure. It aims to fix the main reason that the CSRF attack works. When you send a request from a.com to b.com, the browser adds your cookies for b.com to the request by default. So when the attacker’s code in evil.com sends a money transfer request to bank.com, the browser sends an authenticated request to bank.com. Therefore, the transfer happens.
So what if the browser doesn’t add the cookie to the request? The victim logged in to bank.com. The victim visits evil.com. The code inside evil.com sends a transfer request to bank.com. But since the browser won’t add cookies, the request will go without the cookie header. Transfer won’t happen since there is no authentication.
This was actually a good idea. Therefore, a concept named “SameSite” emerged. SameSite cookie attribute can have three values:
Lax: Cookies are not sent on normal cross-site requests, but are sent when a user is navigating to the origin site (i.e., when following a link).
Strict: Cookies will only be sent in a first-party context and not be sent along with requests initiated by third party websites.
None: Cookies will be sent in all contexts.
So, developers can set Lax or Strict flags to protect against CSRF attacks. But what happens if they don’t do it? Remember our previous example:
“ if the victim uses Chrome browser, CSRF won’t work. If the victim uses Safari, CSRF would work”
The reason of that, Chrome browser actually sets a default SameSite value (Lax) to the cookies. So even if the developer doesn’t know anything about CSRF or SameSite cookies, their websites are protected and CSRF attack won’t work. Let’s try it.
With Chrome browser, navigate to https://authenticationtest.com/simpleFormAuth/ and fill the form with the given username&password values.
You are logged in, the application gave us a session cookie. You can check it in Chrome’s developer console. It doesn’t have any SameSite cookie attribute
There is a form that we can test at https://authenticationtest.com/xssDemo/ . Just fill the textbox and click the “Search” button. It generates a POST request and the request doesn’t contain a random CSRF-token. So in theory, we can conduct a CSRF attack there.
Save the following HTML snippet as csrf.html and open it in the same Chrome browser that you logged in.
<html>
<body>
<script>history.pushState(‘’, ‘’, ‘/’)</script>
<form action=”https://authenticationtest.com/xssDemo/" method=”POST”>
<input type=”hidden” name=”search” value=”requestTest” />
<input type=”submit” value=”Submit request” />
</form>
</body>
</html>
Open Chrome’s developer console and navigate to the “Network” section. Click the “Submit Request” button. What happened? The POST is sent with our authentication cookie:
The full request in Burp proxy
So, that was a lie? Chrome doesn’t set Lax attribute by default? If it does, this request shouldn’t be sent with the cookie.
Things go weird now. Let's remember my first example:
“If the victim logged in to bank.com 2 minutes ago, CSRF will work. If 2 minutes have passed, CSRF won’t work.”
This means, Chrome sends the cookie if the authentication happened 2 minutes ago. After 2 minutes, it won’t send it.
Just wait for 2 minutes and send the request with csrf.html again. Did you see the difference? Cookie isn’t added to the request this time.
Why there is such a weird rule? When Chrome started to set SameSite attribute to Lax by default, it broke some parts of the web.
Some Ouath, OpenID applications, payment gateways etc. require cross-site requests. Without that, the flow won’t be complete. To fix this problem, Chrome developers implemented the 2 minutes rule so that those flows can work. However, this feature will be removed in the future. SameSite=Lax will be the default for everything.
So, if the victim uses a Safari browser and the “bank.com/transfer” endpoint doesn’t require any CSRF tokens, we can exploit it, right.
Well, not always.
Same Origin Policy
I won’t explain what is Same Origin Policy here since it’s a sophisticated concept. I want to discuss its role in CSRF. Let’s start with the wrong misconception that lots of people believe:
“Same Origin Policy restricts a.com to read data from b.com, but you can send requests from a.com to b.com”
This is not very true. SOP restricts reading data, that’s right. But in some cases, it also restricts sending data.
So, what kind of requests does SOP allows and what kind of them are not allowed?
✅It allows sending GET/POST requests through HTML forms. Let’s try the following example:
<html>
<body>
<script>history.pushState(‘’, ‘’, ‘/’)</script>
<form action=”https://example.com/" method=”POST”>
<input type=”hidden” name=”amount” value=”500" />
<input type=”submit” value=”Submit request” />
</form>
</body>
</html>
Request’s content-type will be “application/x-www-form-urlencoded” which is allowed by SOP. Therefore, the request is sent.
❌ It blocks PUT requests. It also blocks the requests that contain “application/json” as Content-Type header.
You won’t be able to send a PUT request, or “Content-Type: application/json” request by using HTML forms. You need to send a “special request”. To send them, you can use XMLHttpRequest (XHR) method of Javascript. Let’s try the following example:
<script>
var xmlhttp = new XMLHttpRequest();
var theUrl = “https://utkusen.com/transfer";
xmlhttp.open(“POST”, theUrl);
xmlhttp.setRequestHeader(“Content-Type”, “application/json;charset=UTF-8”);
xmlhttp.send(JSON.stringify({ “amount”: “500”}));
</script>
The request failed due to “CORS error”. So, what happened behind the scene?
Since this is an XHR request, the browser sends a “preflight” request to the target website with “OPTIONS” method. The browser makes this request to understand if the target website allows this request. How a website can allow this request? With Cross-Origin Resource Sharing (CORS) header of course. If the target website had “Access-Control-Allow-Origin: *” response header, the request would be successful.
So, we can say that:
- If the “bank.com/transfer” endpoint only accepts “application/json” content-type, it doesn’t need any CSRF tokens. The attack won’t work.
- If the “bank.com/transfer” endpoint only accepts “PUT” requests, it doesn’t need any CSRF tokens. The attack won’t work.
But of course, we shouldn’t forget that there might be wrong implementations on the code base. What if “bank.com/transfer” accepts GET requests as well mistakenly? We can exploit it with:
<img src=”https://utkusen.com/?amount=500">
img or script tags don’t require preflight requests.
Conclusion
Technologies develop over time and some vulnerabilities become history. For example, MiTM attacks. It was very easy to do it 10 years ago, now it’s almost not possible. The same thing is happening for the CSRF as well. It gets harder and harder to exploit every day. At some point, it will be almost impossible. Let’s stick with the facts but be careful as well. Cheers.