- Teo Selenius
- Follow @TeoSelenius
What are CSRF vulnerabilities?
They can also happen when a web application misuses the GET method by using it to make changes. Both of these scenarios allow for a malicious website to perform unwanted actions on the logged-in user's behalf.
How to prevent CSRF vulnerabilities?
Follow these steps.
- Use CSRF tokens provided by your application platform/library.
- Don't make changes using other HTTP verbs than POST, PUT, PATCH, or DELETE.
- Secure your cookies with the 'SameSite' directive.
Same Origin Policy
Before we get started, it's helpful to understand SOP (Same Origin Policy), which is the heart and soul of the web browser security model. It is a rule that more or less says:
- Two URLs are of the same origin if their protocol, port (if specified), and host are the same.
- A website from any origin can freely send
OPTIONSrequests to any other origin. Furthermore`, the request will include the user's cookies (including the session ID) to that origin.
- While sending requests is possible, a website from one origin cannot directly read the responses from another origin.
- A website can still consume resources from those HTTP responses, such as by executing scripts, using fonts/styles, or displaying images. The JSONP hack takes advantage of this fourth rule (don't use JSONP).
- A website can from one origin can have restricted access to a window in another origin, if it gets a handle to the window. Most notably windows from different origins can change each other's URL (
For example, this website's origin is
https://www.appsecmonkey.com/ where the
https, the host is
www.appsecmonkey.com, and the port is not specified (which is implicitly
443 because of the
A simple example
Let's pretend that users could log in to AppSec Monkey and update their email address like so:
POST /user/update-email/ HTTP/1.1 Host: www.appsecmonkey.com Cookie: SessionId=ABC123 ... email@example.com
The backend code would perhaps look like this (at least if you use Django):
def update_email(request): new_email = request.POST['new_email'] set_new_email(request.user, new_email)
Now let's say there's an evil website
evil.example.com with the following HTML form and auto-submit script:
When a user that is currently logged in to
www.appsecmonkey.com enters the malicious website, the HTML form is auto-submitted on the user's behalf, and the following HTTP POST request gets immediately sent to
POST /user/update-email/ HTTP/1.1 Host: www.appsecmonkey.com Cookie: SessionId=ABC123 ... firstname.lastname@example.org
And Bob's email address gets changed to
The usual way to protect against these attacks is to use something called a CSRF token. It works so that you add a hidden value, not guessable by an attacker, inside your HTML forms.
<form method="POST" action="/user/update-email/"> <input type="text" name="new_email" /> <input type="hidden" name="csrf-token" value="SomeRandomValue123456" /> </form>
When another website like
evil.example.com tries to submit the form, the webserver rejects the POST request because it doesn't contain the user's CSRF token.
That's your first defense.
Don't forget the login endpoint
It is also a vulnerability, often exploitable in some way, if malicious websites can unwittingly log your user in with their account.
To prevent this, make sure to use CSRF tokens for the login form as well.
You should tie the token to the pre-authenticated session (which you should discard when the user authenticates and give the user a new session ID, but that's another story).
Or, if you use OAuth/OIDC, make sure you are verifying the
state parameter correctly (see here for more on that).
In both scenarios, modern application frameworks should be able to handle it without any hassle.
Mind your HTTP verbs
CSRF-token mechanisms tend to protect POST requests only. So if you accept a GET request such as
https://email@example.com, you will generally be vulnerable regardless of any CSRF protection that you might use.
Make sure you do not use anything except POST/PUT/PATCH/DELETE for making changes.
Modern platforms tend to be explicit about the verb, but you sometimes have to be careful with legacy frameworks.
Use SameSite cookies
These days browsers support a cool feature called SameSite cookies. When you set a cookie with
SameSite=Lax, browsers will not include it in cross-site POST requests.
Set-Cookie: SessionId=123; ...other options... SameSite=Lax
That's it, simple and effective. But do not rely on this feature alone for your application's security. Use CSRF tokens as your primary defense and apply SameSite cookies as a bonus layer of protection.
You can also set the cookie in
SameSite=Strict mode. In this case, also GET requests will be protected. However, this shouldn't be necessary for CSRF protection if you follow the above rule of minding your HTTP verbs. And it breaks functionality somewhat, as links to your application will no longer work as expected (user won't be logged in anymore when the tab/window opens).
Strict variant has the benefit of protecting against some XSS (Cross-Site Scripting) attacks so you might want to at least consider it.
Use cookieless session management
CSRF attacks can be dangerous. Luckily, they are also easy to avoid as long as you use a decent, modern application platform that supports CSRF tokens and is explicit about HTTP verbs. SameSite cookies provide an excellent additional security layer for your application but should not be relied upon for security alone.