CSRF vulnerabilities: How to exploit and how to defend
The past few weeks I implemented an API that relies on Cookies for authentication. When working with Cookies you should always be extremely careful not to introduce CSRF vulnerabilities.
Usually, I build APIs using a specific Authentication
HTTP header which completely removes the risk of CSRF (more on that below). So, why should you be careful when building Websites and APIs which rely on Cookies for authentication? How to attack CSRF vulnerabilities? And how to defend?
Let's find out!
This post contains excerpts from my book Black Hat Rust
What are CSRF vulnerabilities?
A Cross-Site Request Forgery is a vulnerability that allows an attacker to force a user to execute unwanted actions.
How?
When Cookies are used for authentication, they are automatically sent for any request to the domain they are set for, as opposed to the Authentication
header, which should be set client-side for each request.
There are two families of CSRF vulnerabilities:
The first one is by using forms. Imagine a scenario where an application allows users to send money. Something like:
$from = $_COOKIE["user"];
$to = $_POST["to"];
$amount = $_POST["amount"];
transfer_money($from, $to, $amount)
If I host on my website malicious.com
a form like:
<html>
<body>
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="SylvainKerkour" />
<input type="hidden" name="amount" value="1000000" />
</form>
<script>
document.forms[0].submit();
</script>
</body>
</html>
I'm now the happy receiver of $1,000,000 for each customer of bank.com
visiting my website.
The second one is by using URLs. The vulnerability lies in the fact that GET
requests may execute actions instead of read-only queries.
By hosting the following HTML snippet:
<img src="https://bank.com/transfer?to=SylvainKerkour&amount=1000000">
An attacker can make any visitor triggers the following request sending the associated Cookies:
GET https://bank.com/transfer?to=SylvainKerkour&amount=1000000
Any logged-in bank.com
customer visiting this URL would become a happy sponsor of me :)
Why JSON APIs are mostly immune against CSRFs?
HTML forms are limited to two methods: GET
and POST
and to three content types: application/x-www-form-urlencoded
, multipart/form-data
, and text/plain
.
Thus, a form can't make a request with the application/json
and an attacker would need to use an XMLHttpRequest to do that. XMLHttpRequests are subject to the Cross-Origin Resource Sharing (CORS) policy, which block cross-origin requests by default. Thus, you can't make an XMLHttpRequests from attacker.com
to bank.com
if bank.com
doesn't specifically allow it.
That being said, if your JSON API use Cookies, doesn't check that the Content-Type
header is set to application/json
and / or if you have an Access-Control-Allow-Origin
: *
policy, your JSON API might be vulnerable against CSRFs.
A few CSRF case studies
I've curated a few reports about real-world exploitation of CSRF vulnerabilities so you can better apprehend what they look like in the wild:
How to defend against CSRF
Read-only GET
requests
The first line of defense is to never allow GET
request to perform CREATE / UPDATE / DELETE operations.
SameSite
Cookies
The second line of defense is to use the SameSite
Cookie attribute set to Lax
or Strict
. Unfortunately, this feature is today supported by the browsers of 'only' ~93% of internet users, so it shouldn't be used alone.
Build JSON APIs
The third line of defense is to only build JSON APIs with a strict CORS policy ( [app.mysite.com, mysite.com]
for example) and check that the Content-Type
header is set to application/json
.
Check the Origin
The fourth line of defense is to check the Origin
and Referer
headers that indicate the domain from where the request originates. If the request comes from a domain that is not yours, send back a permission denied error.
Anti-CSRF tokens
The last line of defense is Anti-CSRF tokens.
When a user requests a page with a form, the server generates a random token and embeds it into the form as a hidden field. When the user submits the form, the token is sent with the other fields of the form, and the server then checks that the token is valid.
This method is way harder to get right than the others, and I don't recommend implementing it yourself but using a popular library, such as github.com/gorilla/csrf for Go.
Want to learn more about applied security? Get my book Black Hat Rust.