潘忠显 / 2021-04-22
“JavaScript 工作原理”系列文章是翻译和整理自 SessionStack 网站的 How JavaScript works。因为博文发表于2017年,部分技术或信息可能已经过时。本文英文原文链接,作者 Alexander Zlatkov,翻译 潘忠显。
How JavaScript works: CSRF attacks + 7 mitigation strategies
Feb 9 · 12 min read
This is post # 22 of the series, dedicated to exploring JavaScript and its building components. In the process of identifying and describing the core elements, we also share some rules of thumb we use when building SessionStack, a JavaScript application that needs to be robust and highly-performant to help companies optimize the digital experience of their users.
Overview
Cross-Site Request Forgery (CSRF, sometimes pronounced “sea-surf”), also known as one-click attack or session riding is a type of malicious attack on a web app or website. In these types of attacks, the attacker performs malicious requests on behalf of the victim. There are many ways in which a malicious web app can transmit such requests such as specially-crafted image tags, hidden forms, AJAX requests, etc. They all work without the user’s interaction or even knowledge.
While Cross-Site Scripting (XSS) attacks exploit the trust a user has for a particular web app, CSRF attacks exploit the trust a web app has in a particular user’s browser.
Internals of CSRF attacks
When a CSRF attack is being performed, the victim is submitting a malicious request which they were not aware of. This may cause actions to be performed on the web app that can include client or server data leakage, change of session state, or manipulation of an end user’s account.
CSRF attacks are an example of a confused deputy attack against a web browser because the web browser is tricked into submitting a forged request by a less privileged attacker.
CSRF commonly has the following characteristics:
- Involves web sites or web apps that rely on a user’s identity.
- Exploits the site’s trust in that identity.
- Tricks the user’s browser into sending HTTP requests to a target site.
- Involves HTTP requests that have side effects.
This is an overview of the steps in a CSRF attack:
- The victim performs an action, like visiting a web page, clicking a link, etc. which is controlled by the attacker.
- This action sends an HTTP request to a web app on behalf of the victim.
- If the victim has an active authenticated session on the web app, the request is processed as a legitimate request sent by the victim.
It’s important that the victim must have an active session with the web app, which is being attacked through a CSRF exploit.
In most cases, CSRF attacks don’t steal private information, but rather trigger some form of a change related to the victim’s account, such as changing their credentials or even perform a purchase. Forcing the victim to retrieve data from the server doesn’t benefit the attacker because the attacker doesn’t receive the response. The victim does. As such, CSRF attacks target requests that perform changes.
Typically, session management in web apps is based on cookies. With each request to the server, the browser sends the related cookie that identifies the current user’s session. This usually happens even if the request originated from a different web app and domain. This is the vulnerability exploited by the attacker. Although CSRF is normally described in relation to cookie-based session handling, it also arises in other contexts where the application automatically adds some user credentials to requests, such as HTTP Basic authentication and certificate-based authentication.
Example
Let’s look at the following example which illustrates a simple “Profile page” on a social network web app:
This page simply loads the user profile data from the server and populates it into the form. If the form is edited, the data can be submitted and updated by the server.
The server accepts the submitted data only if the user is currently authenticated.
Now let’s look at a malicious page that performs the CSRF attack. This page is created by the attacker and is hosted on a different domain. The goal of this page is to execute a request to the social network app on behalf of the victim, relying on the fact that the victim is authenticated:
The page contains a form with hidden fields. That form’s action points to the same endpoint as the profile page of the social network.
Once the victim opens the malicious website, the form is automatically submitting data to the server of the social network app by the script in the page.
This form is harmless when the user of the social network app is not authenticated. The vulnerable web app will refuse to change the user’s profile because of this missing authentication data. However, if the user is authenticated, the change will be applied as any other legitimate request.
This behavior is due to a cookie on the user’s browser that tracks the current session on the social network. When the vulnerable web app receives the change request, it appears legitimate since it has the correct session cookie.
CSRF Attack
So, even if the attacker has no direct access to the vulnerable web app, they exploit the victim and the CSRF vulnerability to perform unauthorized actions. In fact, unlike what may happen in XSS attacks, here, the attacker doesn’t directly read the cookie and steal it.
This example is a very oversimplified and mature attack that can be much more complex and less visible to the victim. For example, a CSRF attack can be embedded into an iframe and the victim will not be aware that an attack is occurring at all.
There are a series of approaches that should be followed in order to mitigate the risk of CSRF attacks.
Token-Based Prevention
This defense is one of the most popular and recommended methods to mitigate CSRF attacks. It can be achieved with two general approaches:
- Stateful — synchronizer token pattern
- Stateless — encrypted or hashed based token pattern
Many popular frameworks provide out-of-the-box implementations for these techniques.
Built-In CSRF Implementations
It is strongly recommended to research if the framework you are using has an option to achieve CSRF protection by default before trying to build your custom system. Even if there is such a system, there is still some responsibility left for you to configure it properly, such as key management and token management.
If it is not possible to use built-in CSRF protection mechanisms in the framework you are using, you can build them on your own.
Let’s take a look at a built-in CSRF implementation in Express.
Express provides a middleware called csurf
which is all that is needed to do the job.
We won’t go into details about Express or how to install packages in this article.
Here is our index.js
:
const express = require('express');
const bodyParser = require('body-parser');
const csrf = require('csurf')
const cookieParser = require('cookie-parser')
const app = express();
const csrfProtection = csrf({ cookie: true });
app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: true }));
app.set('view engine', 'ejs');
app.get('/', csrfProtection, (req, res) => {
res.render('index', { csrfToken: req.csrfToken() });
});
app.post('/profile', csrfProtection, (req, res, next) => {
res.send(req.body.name);
});
app.listen(3000);
Then in the views
folder, we add index.ejs
which looks like this:
The /
route will render the index.ejs
template with the csrfToken
variable available in the template to interpolate the CSRF token.
In index.ejs
, the csrfToken will be set as a value to the hidden field.
When the form is submitted, a request is being sent to the /profile
route which is CSRF-protected.
Without the CSRF token, an invalid CSRF token error will be thrown.
Synchronizer Token Pattern
The Synchronizer Token Pattern allows the server to validate requests and ensure that they are coming from a legitimate source.
The pattern works by generating tokens on the server for each user session or each request.
When a request is sent from the client, the server must verify the existence and validity of the token in the request compared to the token found in the user session.
In most web apps, servers are using HTTP session objects to identify the logged in users. In this case, a session is generated on the server-side and a session ID is passed to the client. This session ID is most of the time saved in a client-side cookie.
If the cookie which stores the session ID is not protected with advanced configurations (httponly, samesite, secure, etc.), it is possible to access this cookie from another page that is open in the browser.
The per-request approach is more secure since the attacker has less time to interfere and exploit the token. The downside of this approach is that it can potentially damage the customer experience. If the user clicks the “Back” button in the browser, the previous page may contain a token that is no longer valid. This means that interactions with the previous page will result in the server’s inability to validate the token and the requests won’t pass.
CSRF token should have the following characteristics:
- Uniqueness per session
- Hard to predict — a securely generated random value
CSRF tokens can mitigate CSRF attacks because without a token, the attacker cannot create valid requests which will be executed on the server.
CSRF tokens should not be transmitted using cookies, due to potential interception or access by attackers.
It’s also not advisable to transmit CSRF tokens through “GET” requests, since leakage is possible through several locations, such as the browser history, log files, Referrer headers if the protected site links to an external site.
CSRF tokens should be transmitted through:
- Hidden fields used in forms
- Headers used in AJAX calls
Here is how a CSRF token can be added to a form:
The token which is a value of the input field is generated on the server, similarly to the Express example above.
Encryption-Based Token Pattern
As the name suggests, the Encryption-Based Token Pattern is based on encryption. It is a more suitable approach for modern web apps, which do not maintain any state at the server.
The token is generated by the server and is composed of the session ID of the user and a timestamp. This pair is encrypted using a secret key. Once the token is generated, it is returned to the client. As with the Synchronized Token, the Encryption-Based Token is either stored in a hidden field or added to the header for AJAX requests.
Once requests are made with the token, the server tries to decrypt it with the same key which was used for encryption.
If the server is not able to decrypt the token, it means that there was some form of intrusion and the request is treated as malicious or invalid. If the server successfully decrypts the token, the session ID and the timestamp are extracted. The session ID is compared against the currently authenticated user, and the timestamp is compared against the current server time to verify that it’s not beyond the pre-defined expiry time.
If the session ID matches the current user and the timestamp is not expired, the request is treated as secure.
SameSite Cookies
SameSite is a cookie attribute that aims to mitigate CSRF attacks.
This attribute allows the browser to decide whether to send cookies along with cross-site requests. The possible values are:
Strict
— Cookies will only be sent in a first-party context and not be sent along with requests initiated by third-party websites. This means that if there is a link on some website to a private GitHub repository, GitHub will not receive the session cookie if the link is clicked and the user will not be able to access the repository.Lax
— Cookies are not sent on CSRF-prone request methods such asPOST
. Cookies are sent when a user is navigating to the origin site. This is the default cookie value if SameSite has not been explicitly specified in recent browser versions. If there is a link on some website to a private GitHub repository, GitHub will receive the session cookie and the user will be able to access the repository.None
— Cookies will be sent in all contexts such as first-party and cross-origin requests. Additionally, theSecure
flag will be required.
All desktop browsers and almost all mobile browsers now support the SameSite attribute.
This attribute should not replace having a CSRF Token. Instead, it should co-exist with that token in order to protect the user in a more robust way.
Verifying Origins
This mitigation technique consists of two steps which rely on examining the HTTP request header value:
- Determining the source origin — where the request is coming from. This can be done through the
Origin
orReferer
headers. - Determining the target origin — where is the request going.
The server has to verify that the source origin and target origin match. If there is a match, the request is considered legitimate and is accepted. If there is no match, the request is discarded, since the request originated from cross-domain. These headers are reliable since they cannot be altered programmatically through JavaScript as they fall under the forbidden headers which can only be modified by the browser.
Double Submit Cookie
The Double Submit Cookie mitigation approach is an alternative to the CSRF token approach. It is a stateless approach.
When a user visits a web app, a cryptographically strong pseudorandom value should be generated and set as a cookie on the user’s machine, separate from the session ID.
The server then requires that every request includes this value (through a hidden form or request parameter). If both of them match on the server side, the server accepts it as a legitimate request and if they don’t, it would reject the request.
Custom Request Headers
This approach is well suited for web apps that are heavy on AJAX requests and rely on API endpoints.
The Same-Origin Policy is used in this approach, which restricts that only JavaScript can be used to add a custom header, and only within its origin. By default, browsers do not allow JavaScript to make cross-origin requests with custom headers.
The efficiency of this solution requires a robust CORS configuration since custom headers for requests coming from other domains trigger a pre-flight CORS check.
This allows you to add a custom header to your requests and simply verify its presence and value on the server.
This technique works for AJAX calls, but <form>
elements should be additionally protected with tokens.
Interaction-Based Defense
User behavior can be a very efficient mechanism to prevent unauthorized operations such as CSRF attacks. There are two very common approaches:
- Re-Authentication — enforce the user to authenticate before the request is performed,
- CAPTCHA
While these approaches are very strong against CSRF attacks, they can create a significant impact on the user experience. They should mainly be applied for critical operations such as money transfers.
Pre-Authentication Defense
CSRF attacks are possible even on pages such as login forms where the user is still not authenticated. The impact of such attacks on pre-authenticated pages is different compared to post-authenticated pages.
Let’s consider an e-commerce website, where the victim is browsing through the items, prior to authenticating themselves. An attacker can use a CSRF attack on that website to authenticate the victim with the account of the attacker. When the victim enters their credit card information, the attacker will be able to purchase items using the victim’s card.
In order to mitigate these attacks, you can create pre-sessions while the user is still not authenticated. The login form must include a CSRF token, following the techniques mentioned in the Token-Based prevention section above.
Once the user is authenticated, the pre-session should be transitioned to a real session.
If a customer is complaining that something in their account is not as expected, it’s possible that they have been a victim of a CSRF attack. A platform like SessionStack allows you to easily pinpoint user sessions and replay them as videos to see exactly what happened. This will show you whether the user was responsible for the changes in their account or if it was some external interference.
There is a free trial if you’d like to give SessionStack a try.
SessionStack replaying a user session
If you missed the previous chapters of the series, you can find them here:
- An overview of the engine, the runtime, and the call stack
- Inside Google’s V8 engine + 5 tips on how to write optimized code
- Memory management + how to handle 4 common memory leaks
- The event loop and the rise of Async programming + 5 ways to better coding with async/await
- Deep dive into WebSockets and HTTP/2 with SSE + how to pick the right path
- A comparison with WebAssembly + why in certain cases it’s better to use it over JavaScript
- The building blocks of Web Workers + 5 cases when you should use them
- Service Workers, their life-cycle, and use cases
- The mechanics of Web Push Notifications
- Tracking changes in the DOM using MutationObserver
- The rendering engine and tips to optimize its performance
- Inside the Networking Layer + How to Optimize Its Performance and Security
- Under the hood of CSS and JS animations + how to optimize their performance
- Parsing, Abstract Syntax Trees (ASTs) + 5 tips on how to minimize parse time
- The internals of classes and inheritance + transpiling in Babel and TypeScript
- Storage engines + how to choose the proper storage API
- The internals of Shadow DOM + how to build self-contained components
- WebRTC and the mechanics of peer to peer connectivity
- Under the hood of custom elements + Best practices on building reusable components
- How JavaScript works: exceptions + best practices for synchronous and asynchronous code
- How JavaScript works: 5 types of XSS attacks + tips on preventing them
Resources: