Pilcrow

Auth book

Cross-site request forgery (CSRF)

Cross-site request forgery (CSRF) is an attack where a malicious website sends authenticated requests to an application where the user is currently signed in, allowing the attacker to execute unauthorized actions on their behalf.

Browsers automatically attach relevant cookies to all outgoing requests, including those initiated by malicious third-party websites. While the browser's same-origin policy (SOP) blocks most cross-origin requests, it explicitly permits "simple requests". These include GET, HEAD, and certain POST requests. If your server accepts these requests and store the session token inside a cookie, a malicious website can send requets as the authenticated user. Note that the vulnerability is specifically the browser’s automatic use of the user's cookies. A random person manually sending requests with tools like cURL is not a CSRF vulnerability and should not be treated as an attack.

First, it’s important to clarify the difference between “origin” and “site.” In the context of same-origin and cross-origin, an origin refers to the exact domain, including the subdomain. foo.example.com and bar.example.com are considered different origins. A site, however, refers to the root domain. foo.example.com and bar.example.com are considered same-site. Note that browsers also take the Public Suffix List into account when determining the root domain. For example, github.io, which is used by GitHub Pages to host user-created sites, is included in this list, so foo.github.io and bar.github.io are treated as different sites.

The first rule of CSRF prevention is to never accept GET requests for state-changing actions. Even if you block GET requests triggered with JavaScript, a malicious site can still redirect a user to the URL and trigger the action. Always use POST or other non-GET methods for endpoints that modify state.

Not all POST requests are considered simple requests. POST requests are only treated as such if they completely lack a Content-Type header, or if that header matches one of 3 MIME types: application/x-www-form-urlencoded, multipart/form-data, or text/plain. Note that the browser uses the header instead of the request body to differentiate requests. You must properly parse and validate the header even if you only accept other data types like JSON. You must also reject requests that don't include the header. Do not use a simple string match for validating the header. For example, a header value of text/plain; application/json has the MIME type text/plain and will be allowed by the browser. Even with proper validation, I still recommend taking further CSRF mitigations.

The simplest method to prevent CSRF is to set the SameSite cookie attribute to Strict or Lax. Strict ensures the cookie is not included in any cross-site requests, while Lax ensures that cookie is only included in GET and GET-like request (HEAD, OPTIONS, TRACE). Using Strict will block the cookie from being sent when the user is redirected from a third-party site, which is usually not the desired behavior.

While the SameSite cookie attribute is a good layer of defence, it is unforunately not a silver bullet. First, it will not block cross-origin requests such as between subdomains. You are still susceptible to CSRF if your one your subdomains has a XSS vulnerability or gets taken over. Additionally, the attribute was only widely adopted by browsers around 2019. Any legacy browsers that don't support it will simply ignore the attribute, leaving those users exposed.

As such, I recommend implementing a strict origin check on the server and rejecting non-GET requests originating from untrusted origins. The simplest way to do this is by checking the Sec-Fetch-Site header. This header is included in all browser requests (not just those sent with the Fetch Web API) and indicates the relationship between the initiating website and the target server. Because it is a forbidden header, client-side JavaScript cannot modify it. For non-GET requests, reject any request where the header is missing or the value is not same-origin. Although the header has only been widely supported since 2023, unlike the SameSite cookie attribute, you can still block older browsers by simply rejecting requests that do not include the header. Headers prefixed with Sec- have also been part of the browser forbidden header list since 2008 so the header cannot be spoofed even in legacy browsers. Note that it doesn't matter that the header can be spoofed outside of browsers as CSRF is fundamentally a browser-based attack.

If you need to support older browsers or allow requests from subdomains, use the Origin header. This includes the origin of the request source and has been part of the forbidden request headers since 2008. All major browsers have included this header on most requests since around 2020 at the latest, though some adopted it earlier. Block requests that do not include the header or that originate from sources not present in a list of trusted origins.

Finally, the oldest way to prevent CSRF is by using anti-CSRF tokens. Your server generates a token on the initial visit, and that token is required for any valid form submission. Third-party websites cannot access the token because the browser’s SOP prevents cross-origin sites from reading responses and thus the embedded token. This approach is compatible with legacy browsers and remains widely effective. See OWASP’s CSRF page to learn how to implement them.