What CSRF is #
Cross-Site Request Forgery (often shortened to CSRF or XSRF) is a type of attack in which an external site makes a request to another site on behalf of a user without consent. This attack often relies on there being an existing session on the target site, which the attacker hijacks for their own purposes. Traditionally, this attack also relies on cookies (classic CSRF). However, a new attack variant, Client-Side CSRF (CSCSRF), is possible on sites that use client-side scripting without appropriate input validation.
Although any site could potentially become a target for CSRF attacks and should therefore implement appropriate mitigations, some factors that can increase the likelihood of becoming a target are:
- Having a large audience, where it is likely that users are logged in (for example, major social media sites)
- Being a high-value site where a successful attack is lucrative (for example, sites of financial institutions)
- Offering subdomains to third parties (for example, some blogging platforms and shared hosting services). This is a high-risk scenario because some of the protections implemented in browsers to isolate origins may not work.
Examples #
Classic CSRF: a concrete example #
Cookies are used to store user sessions (or session identifiers) in a vast number of sites, and historically cookies are sent with all requests to a site, even those triggered from external sites.
Imagine the site bank.example
, which works with user sessions. When a client logs in, the site sets a cookie like SESSIONID
with some session identifier. Although the session identifier alone is enough to make requests on
behalf of a (logged-in) client, it is sufficiently difficult to guess for an attacker, so that directly exploiting this mechanism is impractical. The site at bank.example
offers transfer functionality, which works by submitting a form to the
address bank.example/secure/transfer.do
with the transfer amount and a destination account.
How could an attacker exploit this system to transfer funds to themselves? Imagine further that an attacker has compromised an unrelated site, vulnerablenewssite.example
, which ideally (for the attacker) is a site that the bank’s customers
are likely to visit. Then the attacker can make a request from vulnerablenewssite.example
to bank.example/secure/transfer.do
with their account details by submitting a form. This request will include the users’
SESSIONID
cookie (which the attacker doesn’t know) and result in the attacker stealing funds from the unsuspecting user.
Bonus classic CSRF example: logging out #
Another example of a CSRF attack is terminating users’ sessions. Many sites implement logging out by directing the user to a location like example.com/logout
, which implements the logic necessary to invalidate a user session. An attacker
could exploit this fact to terminate a session from an external site by loading this path (if the path also works with a simple GET request, this can be done trivially by embedding the logout path as an external resource in a multitude of ways, such as by
using the link
, img
, object
and iframe
tags, to name a few).
Although in many cases this may not have many consequences besides annoyance (still, depending on the target site even a short-lived denial of service could be advantageous to an attacker, especially in combination with other attacks) at having to log in again, this illustrates two points:
- That the request doesn’t need to fully ‘succeed’ (i.e., probably loading an image from
example.com/logout
will not actually display an image). As long as the request is made, whether or not a valid resource is presented, an attack has the potential to succeed. - Although this attack also works with
POST
requests by submitting a form, allowingGET
requests for things that change the server state in some manner leaves the target site in a precarious position because it opens up more avenues for an attack while reducing the number of possible mitigations discussed later.
Client-Side CSRF: a concrete example #
Client-Side CSRF has similar consequences to classic CSRF but is carried out in a different way (through insecure input validation) that renders most defences against classic CSRF ineffective.
Consider the bank site from earlier. To mitigate classic CSRF attacks, they’ve stopped using cookies in favour of local storage and user actions are implemented with requests using client-side scripting. Because local storage can only be read by
bank.example
and because, unlike cookies, values in local storage are not sent along HTTP requests, a classic CSRF attack will fail.
However, imagine that now, in order to make a transfer, the URL is something like https://bank.example/secure/action.do#?destination=transfer.do&data=amount_100-payee_1234
, for making a transfer of $100 to the account number 1234.
Then, the page at https://bank.example/secure/action.do
has a script that extracts the relevant data from the URI fragment (i.e., the part after #
) and makes a request to https://bank.example/secure/transfer.do
,
which is deduced from these data.
Since all of these parameters are controlled by an attacker, several ways of exploiting this system exist. For example, an attacker could open a new browser window (e.g., through window.open
) to the target URL or could trick the user into
clicking an attacker-crafted link. Because the exploitation happens through client-side code on the target site, server-side or browser-side CSRF mitigations are ineffective.
CSRF mitigations #
SameSite
cookies #
Historically, cookies set on a site are sent along with all requests to that site, even for requests that originate from a different site. This is the mechanism that classic CSRF exploits, and, to mitigate this, RFC6265bis introduced the SameSite
attribute, which is implemented in all major modern browsers.
The SameSite
attribute may take three possible values:
None
, which results in the historical behaviour of the cookie being sent with all requests;Lax
, which results in the cookie being sent only for requests either originating from the same site or navigation to the site (for example, as a result of following a link); andStrict
, which omits to send the cookie unless for requests originating from the same site (i.e., all external requests, including navigation will omit the cookie).
For most sites, SameSite=Lax
offers a good balance between convenience and protection. This is the default behaviour in many major modern browsers (i.e., all cookies implicitly are SameSite=Lax
unless the attribute is
specified to be something else), with the notable exceptions at the time of writing being Firefox and Safari. In all other browsers, including legacy browsers that do not support the SameSite
attribute, the default (or only) behaviour is that
of SameSite=None
.
As the first line of defence, all sensitive cookies should explicitly set the SameSite=Lax
(or SameSite=Strict
) attribute to guard off against classic CSRF and Secure
attributes to guard against classic
CSRF attacks. In addition, sensitive pages should use HTTPS and set the Secure
attribute to protect against other attacks, as well as the HttpOnly
attribute if the cookie does not need to be visible to client-side
scripts.
TLS 1.3 + SameSite
#
Because there is a nearly complete overlap between browsers that support TLS 1.3 and browsers that support SameSite
, making the site available only through TLS 1.3 (or higher) can help ensure that SameSite
is effective by
preventing older browsers from making requests at all. Do note that this isn’t an absolute guarantee since older browsers may still access the site with some non-standard configurations.
CSRF tokens #
The most common technique for CSRF protection, which works in older and newer browsers alike, consists of including some kind of token along with the request. This token needs to be difficult to guess for third parties so that attempts at CSRF fail validation.
Double-submit cookies #
This technique has the advantage of being stateless (i.e., the server does not need to maintain any state related to CSRF protection). It works by:
- Setting a cookie on the client that has sufficient entropy (i.e., a long random value) and having all requests to resources that need to be protected include this value (or a value derived therefrom).
- Upon receiving a request, requiring that the two values match
For example, upon visiting a site, it could set the cookie _CSRF
to the value 01234567-89ab-4000-8000-0123456789ab
. Then, upon receiving requests that result in some sensitive action, the value
01234567-89ab-4000-8000-0123456789ab
(or some derived form) is included as an additional field, which is checked to verify it matches the value included in the cookie. This additional field could, for example, take the form of a hidden form
input field or a custom header.
Sites that may have untrusted subdomains or subpaths (for example, because they are under the control of third parties) should consider restricting access to cookies with appropriate use of the Domain
and Path
attributes.
However, when this is not practical, an appropriate defence is setting the double-sent value to be a value derived from the CSRF cookie in a way that only the target server can validate it. For example, the hidden field can be set to
HMAC(_CSRF cookie, secret)
or even HMAC(_CSRF cookie + path, secret)
.
Additional restrictions #
To reduce the timeframe available to an attacker, the CSRF token cookie could be digitally-signed or integrity-protected, and the validity window set, with the token being rotated before it expires. This has the disadvantage of making validation slightly more demanding on the server as well as potential inconvenience to users when the token expires.
Synchronised tokens #
As an alternative to double-submit cookies, the value used to derive the token can be stored server-side on the server as part of the session information. This allows for finer-grained control of the token and its validation because it remains opaque to the client and any potential attackers. However, it can require more server resources and inconvenience to users when the token expires or is rotated.
Avoiding cookies #
Since classic CSRF attacks rely on how cookies are sent in the HTTP protocol, a viable protection against attacks can be simply not using cookies, opting for local or session storage instead. This may be especially appropriate for sites which require client-side scripts to function and require or can have session identifiers or information available to client-side scripts.
CORS #
Generally, classic CSRF attacks are not prevented by Cross-Origin Resource Sharing (CORS) because for compatibility reasons many cross-origin requests are allowed to go through without CORS checks, such as embedding external resources or submitting forms.
For applications requiring client-side scripts, one simple way of triggering CORS checks is by including a custom header or submitting payloads with a content-type
header that is not allowed in HTML forms, such as
application/json
. The backend must in this case check that incoming requests have the expected headers as well as block cross-origin requests as appropriate.
When using CSFR tokens, CORS must be set up correctly, or else a malicious actor could take advantage of an ineffective policy to retrieve the token value directly.
Origin
and Referer
headers validation #
Browsers prevent spoofing the values of the Origin
and Referer
headers (although they do allow the omission of the Referer
header in some circumstances). The Referer
header is by default included in
all cross-origin requests while generally the Origin
header is included in all CORS requests as well as POST
requests.
One could protect against CSRF attacks by verifying that the value of the Origin
header (when present) or else the origin in the Referer
header (when present) matches an expected value, which usually should be the host same as
that of the Host
header.
In some situations, neither the Origin
nor the Referer
headers are set, one such situation being a GET request caused by direct user navigation. When this happens one may either block (erring on the side of caution) or allow
(erring on the side of availability) the incoming request. A good rule of thumb is blocking all non-GET
/ non-HEAD
requests with neither header while deciding on GET
(and HEAD
) on a case-by-case
basis.
Sec-*
headers validation #
Many, but not all, modern web browsers implement various Sec-
-prefixed request headers, which can provide additional information that helps prevent CSRF attacks. In particular, the Sec-Fetch-Site
having the value
cross-site
(different site altogether) or same-site
(different subdomain) may indicate a CSRF attempt, and these requests may be blocked as appropriate.
Since not all browsers (notably, Safari) send this header along, requests lacking this header should be allowed through to prevent service disruption to legitimate users.
Built-in CSRF protection #
If you’re using some web framework, chances are that it comes with CSRF protection out-of-the-box or that third-party modules exist that provide the functionality. Before implementing your own, you may want to familiarise yourself with the built-in protection to see if it covers your needs since chances are that it will be a robust and well-tested solution that will save you time.
Protection against Client-Side CSRF #
Unlike classic CSRF, client-side CSRF can be more challenging to protect against because rather than checking for the correctness of certain predetermined values, one needs to check for the well-formedness and correctness of application-dependent data.
Some useful guidelines against client-side CSRF can be:
- When possible, avoid client-side actions that affect the application state or are potentially sensitive upon without prior user interaction. In other words:
- If possible, do not automatically make sensitive requests if the user has not interacted with the page (e.g., moving the mouse, clicking on something, typing something, scrolling, etc.)
- When dynamic endpoints are used in a way that a malicious actor could manipulate (for example, as part of the URL), implement validation rules that are as strict as feasible for the application (for example, a whitelist or pattern checking).
- When the data are sent as dynamic client-side requests that could be manipulated by a malicious actor (for example, as part of the URL), implement validation rules that are as strict as feasible for the application (for example, schema validation or data signing).
Other defence-in-depth measures #
Content Security Policy (CSP) #
While not effective against all forms of client-side CSRF (specifically, direct linking or somehow making a user open a link), a well-crafted content security policy can close some attack venues. Specifically:
frame-ancestors
(and the headerX-Frame-Options
) can be used to prevent embedding of our site into a different site, thus forcing an attacker to make the user open a link, which may be more evident than, e.g., a hidden<iframe>
.connect-src
to restrict the origins our site is allowed to connect to, thus preventing the leaking of data to an attacker-controlled siteimg-src
,script-src
,default-src
, etc.: similar toconnect-src
, if we might leak information through dynamically inserted resources.