Cross-Site Request Forgery (CSRF): A Simple Explanation
Cross-Site Request Forgery (CSRF) is a type of attack that occurs when a malicious website tricks a user into submitting a request to a website that they are authenticated to. The attacker can use this request to perform actions on the website on behalf of the user, such as changing their password, deleting data, etc.
Since the browser automatically sends cookies with every request to a website, the request will be authenticated using the user’s cookies. This means that the website will treat the request as if it came from the user, even though it was initiated by the attacker.
A Simple Application to Demonstrate CORS
We’re going to setup a simple application to demonstrate CSRF. We will have a flask application running on https://devbox1.com
as a legit website and another flask application running on https://devbox2.com
as an attacker’s website.
We’re using linux hosts file to serve the application under a domain name. hosts file is a handy way to map domain names to IP addresses. In a production environment, you would use a DNS server to map domain names to IP addresses.
|
|
We can make another step and create a self-signed certificate for the domains and serve the applications over HTTPS. This is not needed for the demonstration.
|
|
|
|
devbox1
|
|
|
|
|
|
devbox2
|
|
|
|
|
|
The login endpoint will set a session cookie with the username. browsers will automatically send the cookies with every request. The cookie’s SameSite
attribute is determines this behavior.
What’s a Cookie? A cookie is a small piece of data that a server sends to the browser. The browser stores the cookie and sends it back with every request to the server. Cookies are used to store user information, so the server can identify the user and provide a personalized experience.
Let’s open https://devbox1.com/login
in the browser and submit a username.
If the site doesn’t load try the following
- Make sure the domain is resolved to the correct IP address. You can check this by running
ping devbox1.com
in the terminal. Note that DNS utilities likedig
don’t use the hosts file unlikeping
and browsers. - Hard refresh the page. If you’re using Chrome, Press
Ctrl + F5
. - If you’re using Crome, go to
chrome://net-internals/#dns
and clear the DNS cache. Browsers have their own DNS cache to speed up the loading of websites.
Then open https://devbox2.com/bait/
in the same browser.
The malicious website will send a request to the legit website. The browser will automatically send the cookies for devbox1.com
with the request because the credentials
flag is set to true
. The legit website will treat the request as if it came from the user, even though it was initiated by the attacker.
You should get a CORS error in the console. This is because the browser blocks the request from https://devbox2.com
to https://devbox1.com
due to the same-origin policy. You can take a look at my previous post on CORS to understand more about it.
|
|
To get around this, we need to enable CORS on the server running on https://devbox1.com
. We need to set Access-Control-Allow-Origin
to include the origin of the request from https://devbox2.com
which is the same value. The *
value can also be used to allow requests from any origin.
Let’s use flask-cors
library to do this.
|
|
|
|
This config raises an error
|
|
The reason being based on the CORS specification, you can’t set the Access-Control-Allow-Origin
header to *
when Access-Control-Allow-Credentials
is set to true
.
Let’s assume the legit website owner wants to allow requests from any origin and they set the header to the request origin. For our demonstration, we can set it to https://devbox2.com
.
|
|
Now if you open https://devbox2.com/bait
in the browser, you should see the response from https://devbox1.com
in the console.
|
|
As you can see, the malicious website was able to send a request to the legit website on behalf of the user. This is a CSRF attack.
This was a simple request that did’t change anything on the server. Let’s demonstrate a more dangerous CSRF attack.
Let’s define a route that sends an invitatioin to a user.
|
|
Add the following code to the attacker’s application.
|
|
If you open https://devbox2.com/bait
in the browser, the attacker’s website will send an invitation on behalf of the user.
How to Prevent CSRF
There’s no foolproof way to prevent CSRF attacks, but there are some measures you can take to reduce the risk.
Use Lax/Strict Cookies
The SameSite
attribute determies when the browser should send cookies with a request. It has three possible values
Strict
: This means that the cookies will not be sent in cross-site requests. the browser first checks the origin of the request and only sends the cookies if the origin matches the current origin in the address bar.Lax
: The browser sends the cookies only when the user navigates to the website from a link or types the URL in the address bar. This means that the cookies will not be sent if the user submits a form or uses a script to send a request. Chrome sets theSameSite
attribute toLax
if it’s not set.None
: The browser sends the cookies with every request, regardless of the origin. for example in iframe requests, images, and scripts.
Check out the MDN for more information on the SameSite
attribute.
Lax
is a good compromise between security and usability and is recommended for most websites.
In our first exmaple, if the legit website sets the SameSite
attribute to Strict
or Lax
, the browser will not send the cookies in which the request will fail. (If you want to try this, don’t forget to clear the cookies in the browser to see the effect.)
To overcome this, the attacker would trick the user to click on a link that sends a GET request to the legit website which the browser will send cookies with SameSite
set to Lax
.
So a recommendation here is to make sure safe methods like GET
do not change a state on the server and are side effect free.
The other layer of security is to use CSRF tokens.
Use CSRF Tokens
A CSRF token is a unique value that verifies that the request was initiated by the website and not by an attacker.
The application generates a token, includes it in the form and stores it in a cookie. When the form is submitted, the token is sent as a form field or a header. the server verifies the token before processing the request with the same token stored in the cookie. since the attacker’s website can’t read the token from the cookie due to the same-origin policy, they can’t include it in the form.
The cookie policy is aligned with the same-origin policy. The browser should block any access to document.cookie
from sites with different origins, even if they share the same cookie domain. For instance, apple.dev.com should not be able to access document.cookie
from banana.dev.com, even though the cookie domain is *.dev.com.
Every web framework has its own way of generating and verifying CSRF tokens. For example Flask has a package called flask-wtf
that can be used to generate and verify CSRF tokens.
|
|
|
|
If you open https://devbox2.com/bait2
in the browser, the request will fail with a 400 Bad Request error.
|
|
This library by default protects all POST, PUT, PATCH, and DELETE requests.
Check out the OWASP for more information on CSRF attacks.