DEV Community

Ricardo Iván Vieitez Parra for Apeleg Limited

Posted on • Edited on • Originally published at apeleg.com

Effectively mitigating CSRF

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), although a new variant of the attack, called 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 sites 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 c 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 potential to succeed.
  • Although this attack also works with POST requests by submitting a form, alloing GET 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 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 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); and
  • Strict, which omits sending 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 a 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:

  1. 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).
  2. Upon receiving a request, requiring that the two values match

For example, upon visiting a site, it could set a 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 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 resource and result in 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 that require client-side scripts, one way 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, it is imperative that CORS 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 to omit 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, it could happen that neither the Origin nor the Referer headers be set, one such situation being a GET request caused by direct user navigation. When this happens, one might decide to either block (erring on the side of caution) or allow (erring on the side of availability) the incoming request. A good guideline might be 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 is helpful in preventing CSRF. In particular, the Sec-Fetch-Site having the value cross-site (different site altogether) or same-site (different subdomain) may be indicative of 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 sort of 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, typed something, scrolled, etc.)
  • When dynamic endpoints are used in a way 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, 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 header X-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 obvious than, e.g., a hidden <iframe>.
  • connect-src to restrict the origins our site is allowed to connect to, thus preventing leaking data to an attacker-controlled site
  • img-src, script-src, default-src, etc.: similar to connect-src, if we might leak information through dynamically inserted resources.

Top comments (0)