Filippo Valsorda

Cross-Site Request Forgery

Cross-Site Request Forgery (CSRF) is a confused deputy attack where the attacker causes the browser to send a request to a target using the ambient authority of the user’s cookies or network position.1 For example, attacker.example can serve the following HTML to a victim

<form action="https://example.com/send-money" method="post">
  <input type="hidden" name="to" value="filippo" />
  <input type="hidden" name="amount" value="1000000" />
</form>

and the browser will send a POST request to https://example.com/send-money using the victim’s cookies.

Essentially all applications that use cookies for authentication need to protect against CSRF. Importantly, this is not about protecting against an attacker that can make arbitrary requests2 (as an attacker doesn’t know the user’s cookies), but about working with browsers to identify authenticated requests initiated from untrusted sources.

Unlike Cross-Origin Resource Sharing (CORS), which is about sharing responses across origins, CSRF is about accepting state-changing requests, even if the attacker will not see the response. Defending against leaks is significantly more complex and nuanced, especially in the age of Spectre.

Why do browsers allow these requests in the first place? Like anything in the Web platform, primarily for legacy reasons: that’s how it used to work and changing it breaks things. Importantly, disabling these third-party cookies breaks important Single-Sign On (SSO) flows. All CSRF solutions need to support a bypass mechanism for those rare exceptions. (There are also complex intersections with cross-site tracking and privacy concerns, which are beyond the scope of this article.)

Same site vs same site vs same origin

To protect against CSRF, it’s important to first define what is a cross-site or cross-origin request, and which should be allowed.

https://app.example.com, https://marketing.example.com, and even http://app.example.com (depending on the definition) are all same-site but not same-origin.

It’s tempting to declare the goal as ensuring requests are simply from the same site, but different origins in the same site can actually sit at very different trust levels: for example it might be much easier to get XSS into an old marketing blog than in the admin panel.

The starkest difference in trust though is between an HTTPS and an HTTP origin, since a network attacker can serve anything it wants on the latter. This is sometimes referred to as the MitM CSRF bypass, but really it’s just a special case of a schemelessly same-site cross-origin CSRF attack.

Some parts of the Web platform apply a schemeful definition of same-site, where https://app.example.com and http://app.example.com are not same-site:

Using HTTP Strict Transport Security (HSTS), if possible, is a potential mitigation for HTTP→HTTPS issues.

Countermeasures

There are a number of potential countermeasures to CSRF, some of which have been available only for a few years.

Double submit or synchronized tokens

The “classic” countermeasure is a CSRF token, a large random value submitted in the request (e.g. as a hidden <input>) and compared against a value stored in a cookie (double-submit) or in a stateful server-side session (synchronized tokens).

Normally, double-submit is not a same-origin countermeasure, because same-site origins can set cookies on each other by “cookie tossing”. This can be mitigated with the __Host- cookie prefix, or by binding the token to the session/user with signed metadata. The former makes it impossible for the attacker to set the cookie, the latter ensures the attacker doesn’t know a valid value to set it to.

Note that signing the cookies or tokens is unnecessary and ineffectual, unless it is binding the token to a user: an attacker that’s cookie tossing can otherwise obtain a valid signed pair by logging into the website themselves and then use that for the attack.

This countermeasure turns a cross-origin forgery problem into a cross-origin leak problem: if the attacker can obtain a token from a cross-origin response, it can forge a valid request.

The token in the HTML body should be masked as a countermeasure against the BREACH compression attack.

The primary issue with CSRF tokens is that they require developers to instrument all their forms and other POST requests.

Origin header

Browsers send the source of a request in the Origin header, so CSRF can be mitigated by rejecting non-safe requests from other origins.

The main issue is knowing the application’s own origin. One option obviously is asking the developer to configure it, but that’s friction and might not always be easy (such as for open source projects and proxied setups).

The closest readily available approximation of the application’s own origin is the Host header. This has two issues:

  1. it may be different from the browser origin if a reverse proxy is involved;
  2. it does not include the scheme, so there is no way to know if an http:// Origin is a cross-origin HTTP→HTTPS request or a same-origin HTTP request.

Some older (pre-2020) browsers didn’t send the Origin header for POST requests.

The value can be null in a variety of cases, such as due to Referrer-Policy: no-referrer or following cross-origin redirects. null must be treated as an indication of a cross-origin request.

Some privacy extensions remove the Origin header instead of setting it to null. This should be considered a security vulnerability introduced by the extension, since it removes any reliable indication of a browser cross-origin request.

SameSite cookies

If authentication cookies are explicitly set with the SameSite attribute Lax or Strict, they will not be sent with non-safe cross-site requests.

This is, by design, not a cross-origin protection, and it can’t be fixed with the __Host- prefix (or Secure attribute), since that’s about who can set and read cookies, not about where the requests originate. (This difference is reflected in the difference between Scheme-Bound Cookies and Schemeful Same-Site.) The risk of same-site HTTP origins is still present, too, in browsers that don’t implement Schemeful Same-Site.

Note that the rollout of SameSite Lax by default has mostly failed due to widespread breakage, especially in SSO flows. Some browsers now default to Lax-allowing-unsafe, while others default(ed) to None for the first two minutes after the cookie was set. These defaults are not effective CSRF countermeasures.

Non-simple requests

Although CORS is not designed to protect against CSRF, “non-simple requests” which for example set headers that a simple <form> couldn’t set are preflighted by an OPTIONS request.

An application could choose to allow only non-simple requests, but that is fairly limiting precisely because “simple requests” includes all the ones produced by <form>.

Fetch metadata

To provide a reliable cross-origin signal to websites, browsers introduced Fetch metadata. In particular, the Sec-Fetch-Site header is set to cross-site/same-site/same-origin/none3 and is now the recommended method to mitigate CSRF.

The header has been available in all major browsers since 2023 (and earlier for all but Safari).

One limitation is that it is only sent to “trustworthy origins”, i.e. HTTPS and localhost. Note that this is not about the scheme of the initiator origin, but of the target, so it is sent for HTTP→HTTPS requests, but not for HTTPS→HTTP or HTTP→HTTP requests (except localhost→localhost). If Sec-Fetch-Site is missing, a lax fallback on Origin=Host is an option, since HTTP→HTTPS requests are not a concern.

Protecting against CSRF in 2025

In summary, to protect against CSRF applications (or, rather, libraries and frameworks) should reject cross-origin non-safe browser requests. The most developer-friendly way to do so is using primarily Fetch metadata, which requires no extra instrumentation or configuration.

  1. Allow all GET, HEAD, or OPTIONS requests.

    These are safe methods, and are assumed not to change state at various layers of the stack already.

  2. If the Origin header matches an allow-list of trusted origins, allow the request.

    Trusted origins should be configured as full origins (e.g. https://example.com) and compared by simple equality with the header value.

  3. If the Sec-Fetch-Site header is present:

    1. if its value is same-origin or none, allow the request;
    2. otherwise, reject the request.

    This secures all major up-to-date browsers for sites hosted on trustworthy (HTTPS or localhost) origins.

  4. If neither the Sec-Fetch-Site nor the Origin headers are present, allow the request.

    These requests are not from (post-2020) browsers, and can’t be affected by CSRF.

  5. If the Origin header’s host (including the port) matches the Host header, allow the request, otherwise reject it.

    This is either a request to an HTTP origin, or by an out-of-date browser.

The only false positives (unnecessary blocking) of this algorithm are requests to non-trustworthy (plain HTTP) origins that go through a reverse proxy that changes the Host header. That edge case can be worked around by adding the origin to the allow-list.

There are no false negatives in modern browsers, but pre-2023 browsers will be vulnerable to HTTP→HTTPS requests, because the Origin fallback is scheme-agnostic. HSTS can be used to mitigate that (in post-2020 browsers), but note that out-of-date browsers are likely to have more pressing security issues.

Finally, there should be a tightly scoped bypass mechanism for e.g. SSO edge cases, with the appropriate safety placards. For example, it could be route-based, or require manual tagging of requests before the CSRF middleware.

Go 1.25 introduces a CrossOriginProtection middleware in net/http which implements this algorithm. (This research was done as background for that proposal.)

Thank you to Roberto Clapis for helping with this analysis, and to Patrick O’Doherty for setting in motion and testing this work. For more, follow me on Bluesky at @filippo.abyssdomain.expert or on Mastodon at @filippo@abyssdomain.expert.

The picture

Back to Rome photoblogging. This was taken from the municipal rose garden, which opens for a couple weeks every spring and fall.

White roses in the foreground, with a grassy park, trees, and the Domus Severiana ruins in the background under a blue sky with scattered clouds.

This work is made possible by Geomys, my Go open source maintenance organization, which is funded by Smallstep, Ava Labs, Teleport, Tailscale, and Sentry. Through our retainer contracts they ensure the sustainability and reliability of our open source maintenance work and get a direct line to my expertise and that of the other Geomys maintainers. (Learn more in the Geomys announcement.)

Here are a few words from some of them!

Teleport — For the past five years, attacks and compromises have been shifting from traditional malware and security breaches to identifying and compromising valid user accounts and credentials with social engineering, credential theft, or phishing. Teleport Identity is designed to eliminate weak access patterns through access monitoring, minimize attack surface with access requests, and purge unused permissions via mandatory access reviews.

Ava Labs — We at Ava Labs, maintainer of AvalancheGo (the most widely used client for interacting with the Avalanche Network), believe the sustainable maintenance and development of open source cryptographic protocols is critical to the broad adoption of blockchain technology. We are proud to support this necessary and impactful work through our ongoing sponsorship of Filippo and his team.


  1. Abuse of the ambient authority of network position, often through DNS rebinding, is being addressed by Private Network Access. The rest of this post will focus on abuse of cookie authentication. 

  2. This is why API traffic generally doesn’t need to be protected against CSRF. If it looks like it’s not from a browser, it can’t be a CSRF. 

  3. none means the request was directly user-initiated, e.g. a bookmark.