What is CSRF?#
Cross-site request forgery allows an attacker to perform any actions that a normal user is able to. These attacks, when performed properly, can cause an end user to execute unwanted actions on a web application that they are authenticated to.
How Does CSRF Work?#
In order for us to perform CSRF, we need to make sure three conditions are being met:
- There needs to be a relevant action for us to forge. This might be a privileged action like changing another user’s permissions or some user specific change like changing a profile picture.
- The action needs to involve one or more HTTP requests and the application must rely solely on session cookies to identify the user who made the request.
- The requests that perform the action do not contain any values that we are not able to guess or predict.
For example, imagine an application lets us change our email using a request like this:
POST /email/change HTTP/1.1
Host: vulnerable.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 30
Cookie: session=yvthwsztyeQkAPzeQ5gHgTvlyxHfsAfE
email=gabe@mail.com
This would meet the conditions because we would want to change the email address of another user, only a session cookie is being used to authenticate the request, and there are no values that we need to guess.
We could then host an HTML page that would submit or forge this action on the user’s behalf when they visit our malicious page:
<html>
<body>
<form action="https://vulnerable.com/email/change" method="POST">
<input type="hidden" name="email" value="evil@hacking.com" />
</form>
<script>
document.forms[0].submit();
</script>
</body>
</html>
The user visits our page and, if they are logged in to the vulnerable site with an active session, the request is submitted as if made by the victim user. (This assumes that SameSite cookies are not being used.)
Performing a CSRF Attack#
We can usually use the Burp Suite Professional feature called CSRF PoC generator to make these kinds of HTML pages based off of a request we would like to forge. It is pretty straight forward to use and if you don’t have a Burp Professional license you can use something like this to get the job done.
Typically these attacks are delivered by having a victim user look at a malicious website or through some XSS attack vector on a vulnerable application. In some cases these vulnerabilities can be fully contained in a GET request to the vulnerable web application, so all an attacker would need to do is get the user to click a link instead of actually going to a page.
Common CSRF Defenses#
There are a variety of common defenses in place for CSRF attacks that will likely need to be circumvented if we are to actually exploit some of those vulnerable applications.
CSRF Tokens#
These are often unique, secret, and unpredictable values generated by some server-side application that is shared with the client at some point. Whenever the user attempts to perform some sensitive action like changing their email address, the client must include this CSRF token that was given to them in the request. This value being a unique, secret, and unpredictable makes it very difficult for attackers to make a valid request.
SameSite Cookies#
These are a browser security mechanism that determines when a website’s cookies are included in requests that originate from other websites. Requests to perform sensitive actions typically require authenticated session cookies. SameSite restrictions may prevent an attacker from triggering malicious actions from another site. Most browsers (like chrome) use Lax SameSite restrictions by default.
Referer-Based Validation#
Some applications use the HTTP Referer header to defend against CSRF, figuring that it is less likely that a malicious request will originate from the application’s own domain. This is generally the least effective of the three we just covered.
What is a CSRF Token?#
As stated before, CSRF tokens are designed to be unique, secret, and unpredictable values that are generated by the server-side application and shared with the client.
When the client is performing some sensitive action, they will need to supply the CSRF token with their request in order for it to be accepted. Most of the time, there are shared with the client as a hidden parameter in an HTML form like this:
<form name="change-email-form" action="/email/change" method="POST">
<label>Email</label>
<input required type="email" name="email" value="gabe@mail.com">
<input required type="hidden" name="csrf" value="50FaWgdOhi9M9wyna81taR1k3ODOR8d6u">
<button class='button' type='submit'> Update email </button>
</form>
If you were to submit this form, it would look like this as a request:
POST /email/change HTTP/1.1
Host: vulnerable.com
Content-Length: 70
Content-Type: application/x-www-form-urlencoded
csrf=50FaWgdOhi9M9wyna81taR1k3ODOR8d6u&email=gabe@mail.com
As you can imagine, normally this would be a pretty solid defense because it would make it very difficult for an attacker to make a valid request without already being inside their target user’s browser.
Common flaws in CSRF Token Validation#
We wouldn’t be talking about CSRF if there was some magical perfect defense that always worked. Lots of the vulnerabilities with the CSRF tokens usually have to do with them being implemented in a way that doesn’t actually integrate the three attributes of a good CSRF token or validate them correctly.
Validation Depending on Request Method#
Some applications might be configured to validate tokens for POST requests but not GET requests. You might be thinking: “Well, can’t we just include the POST body as URL parameters?” and you’d be right!
If you suspect that the validation is only being done for POST requests, try including all that data in a GET request:
GET /email/change?email=pwned@evil-user.net HTTP/1.1
Host: vulnerable.com
Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm
Validation Depending on Token’s Presence#
Some websites might even have a bigger problem, which would be only validating the CSRF token when it is present in the request.
This would be like if you have a gym membership where you need to badge in, but if you forgot your badge the staff would assume you actually had one and let you through.
Pretty straight-forward, we can just omit the CSRF token and submit a request as if we had one.
CSRF Token Not Tied to Session#
Some applications might not validate that the token belongs to the same session as the user who is making the request.
You can imagine this like the gym example, but instead of unique badges they just give everyone the same keycard with no way to map members to card holders. If a sensitive action is something like signing up for a paid class at the gym by writing your name on some paper form, you can see how the keycard provide no security protecting you from other users.
A website might be set up in a way that the when you submit a request, it checks if the token is in a DB table of valid tokens, but doesn’t check who that token is associated with.
If we apply this same logic to a CSRF attack, we would just be grabbing our own CSRF token legitimately and including it in the HTML page we lure the victim to.
CSRF Token Tied to Non-Session Cookie#
Some applications will tie the CSRF token to a cookie, but not a cookie that is tracking a user session. You might see this if an application is trying to implement multiple authentication frameworks that handle different aspects of session management and CSRF protections.
Here is an example:
POST /email/change HTTP/1.1
Host: vulnerable.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 68
Cookie: session=pSJYSScWKpmC60LpFOAHKixuFuM4uXWF; csrfKey=rZHCnSzEp8dbI6atzagGoSYyqJqTz5dv
csrf=RhV7yQDO0xcq9gLEah2WVbmuFqyOq7tY&email=gabe@mail.com
If the CSRF token in the body of the request is linked to the csrfKey cookie instead of the session cookie, an attack would be possible.
In addition to using our own CSRF token in the request, we would also need to inject our csrfKey cookie in the victim’s browser. You might be able to pull this off if you are able to somehow change the victim’s cookie with something like an XSS flaw already existing on the site.
CSRF Token is Duplicated within a Cookie#
Some applications might not even use some server-side record of tokens that have been issued and just assign a cookie that matches the CSRF token like so:
POST /email/change HTTP/1.1
Host: vulnerable.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 68
Cookie: session=pSJYSScWKpmC60LpFOAHKixuFuM4uXWF; csrfKey=RhV7yQDO0xcq9gLEah2WVbmuFqyOq7tY
csrf=RhV7yQDO0xcq9gLEah2WVbmuFqyOq7tY&email=gabe@mail.com
The application would just check if the CSRF token and csrfKey are the same, and allow the request if that condition is true. This can be convenient because of how easy it is to implement and it avoids the need for some server-side state.
The issue here is that if we as an attacker have any way to set a cookie in the victim’s browser, we can just grab a valid cookie (or maybe an arbitrary one) and use that to submit a valid request.
SameSite Cookies#
As mentioned before, this mechanism determines if a website’s cookies are included in requests coming from other websites. This provides partial protection against a variety of cross-site attacks.
Since 2021, Google Chrome set the Lax standard enabled by default if the website does not explicitly set its own restriction level. We will need to figure out how these restrictions work and how we can bypass some of them.
Sites and Origins#
When walking about sites and origins it is pretty easy to get mixed up if you don’t have a clear distinction between the two. For the purpose of SameSite restrictions, a site can be defined as a TLD (top level domain) like .com or .org in combination with one additional part of the domain.
These restrictions determine if a request is same-site or not by the URL scheme and the definition of a site we just defined.
==https://==test.==example.com==
In this case, a request coming from http://text.example.com and going to https://test.example.com would be considered cross-site because the scheme is different.
An origin is more broad than a site, because a site might include multiple domain names but an origin accounts for just one. Two URLs would be considered to have the same origin if they share the same scheme, domain name, and port.
Let’s evaluate some examples:
| From | To | Same-Site? | Same-Origin? |
|---|---|---|---|
https://test.com | https://test.com | Yes | Yes |
https://gabe.test.com | https://evil.test.com | Yes | No, the domain does not match |
https://test.com | https://test.com:8888 | Yes | No, the ports are different |
https://test.com | https://test.org | No, TLD is different | No, the domain does not match |
https://test.com | http://test.com | No, scheme mismatch | No, scheme mismatch |
This is important to realize because if we are able to perform any JS execution we might be able to bypass these protections by making the requests appear to come from the same site.
How Does SameSite Work?#
SameSite lets browsers and website owners specify which cross-site requests should include specific cookies. Because our previous attacks often required a cookie associated with the victim’s authenticated session and were sent from our own site, the attacks would likely fail.
Most major browsers support the following SameSite restriction levels:
- Strict
- Lax
- None
In order to configure these restriction levels, the SameSite and Set-Cookie attributes need to be included in the response header, for example:
Set-Cookie: session=pSJYSScWKpmC60LpFOAHKixuFuM4uXWF; SameSite-Strict
Bypass SameSite Lax Restrictions#
Lax restrictions typically indicate that the browser will send cookies in cross-site requests if these conditions are met:
- The request uses the GET method
- The request came from a top level navigation by the user
You could imagine combining one of our previous tricks of using a GET request instead of a POST request to satisfy the first condition.
We can combine this with some top level navigation on our malicious website to trigger the request
There are some quirks that might help us further though. As stated before, the Lax restrictions aren’t normally sent in cross-site POST requests but sometimes you’ll find time-limited exceptions.
For example, Chrome only applied Lax restrictions after a minute or two to avoid messing with certain SSO mechanisms that rely on cross-site requests. (This does NOT apply to cookies set with the SameSite=Lax attribute though.)
Bypass SameSite Strict Restrictions#
Strict restrictions will not allow the browser to send any cookies in any cross-site requests. This means if the target site is not the same as the site in the victim’s address bar, the cookie will not be included.
We might be able to bypass this mechanism if we find some gadget in the application that triggers additional requests. A good example of this would be gadgets that trigger redirects using some kind of input that we can control.
Browsers often don’t consider client-side redirects as being any different from normal requests, so the cookies would be send without any issues. If we can get this secondary request to perform the malicious action for us, we wouldn’t have to worry about the SameSite restrictions.
Referer-Based Defenses#
Lots of applications utilize the Referer header to try and defend against CSRF by verifying that the request originated from the application’s own domain.
The Referer header is an optional header that contains a URL for the web page that linked to the resource that is being requested in the current request. Lots of browsers automatically add this when users submit requests by clicking on a link or submitting a form.
Validation of Referer Depends on Header Presence#
Sometimes applications validate the header when it is present but don’t when the same header is absent. If we wanted to exploit CSRF in this circumstance, we would just need the request we are making to drop the Referer header.
We can do this with a META tag in our HTML page:
<meta name="referrer" content="never">
Circumventing Referer Validation#
We might be able to bypass the validation all together by taking advantage of some flaw in how it is used. If the application validates that the domain in the Referer header starts with or contains a certain value, we might just be able to use a subdomain or URL with the specified text inside and completely bypass the validation.
HTB - Bankrobber#
CSRF takes many forms and one of the most interesting ones I’ve found on HTB was on the machine called Bankrobber. I have a writeup for it in my XSS - Applied Review post.
Prevention#
Use CSRF Tokens#
Straight up just include CSRF tokens within relevant requests while keeping in mind the criteria:
- The token must be unpredictable, or very difficult to guess
- The token must be tied to the user’s session
- The token needs to be strictly validated before the sensitive action is performed.
You should aim to use an up-to-date cryptographically secure random number generator to make the token values and maybe concatenate some user-specific value with them to make it harder for attackers to interpret.
Try to transmit these as a hidden input field in the HTML as far away as possible from user input to avoid potential issues with attackers manipulating the page content.
An alternative approach, of placing the token into the URL query string, is somewhat less safe because the query string:
- Is logged in various locations on the client and server side;
- Is liable to be transmitted to third parties within the HTTP
Refererheader; and - Can be displayed on-screen within the user’s browser.
You should aim to validate these tokens by storing them server-side within the user’s session data. Subsequent requests should verify that the token being used matches that which is associated with the user’s session. This validation must be performed regardless of the HTTP method or content type of the request.
Use Strict SameSite Cookie Restrictions#
In addition to proper token validation, you’ll want to set those SameSite restrictions for each cookie that you use. This allows more fine-grained control over the context in which the cookie will be used in.
Ideally you try to use Strict by default, and only lower it to Lax if there is a legitimately good reason for it.