Skip to content
Open
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 93 additions & 84 deletions cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ In short, the following principles should be followed to defend against CSRF:

- **See the OWASP [XSS Prevention Cheat Sheet](Cross_Site_Scripting_Prevention_Cheat_Sheet.md) for detailed guidance on how to prevent XSS flaws.**
- **First, check if your framework has [built-in CSRF protection](#use-built-in-or-existing-csrf-implementations-for-csrf-protection) and use it**
- **If the framework does not have built-in CSRF protection, add [CSRF tokens](#token-based-mitigation) to all state changing requests (requests that cause actions on the site) and validate them on the backend**
- **If the framework does not have built-in CSRF protection, add [CSRF tokens](#token-based-mitigation) to all state changing requests (requests that cause actions on the site) and validate them on the backend, or validate [Fetch Metadata headers](#fetch-metadata-headers) on the backend for all state-changing requests.**
- **If your software is intended to be used only on modern browsers, you may rely primarily on [Fetch Metadata headers](#fetch-metadata-headers) to block cross-site state-changing requests**
- **Stateful software should use the [synchronizer token pattern](#synchronizer-token-pattern)**
- **Stateless software should use [double submit cookies](#alternative-using-a-double-submit-cookie-pattern)**
- **If an API-driven site can't use `<form>` tags, consider [using custom request headers](#employing-custom-request-headers-for-ajaxapi)**
Expand Down Expand Up @@ -151,6 +152,97 @@ Since an attacker is unable to access the cookie value during a cross-site reque

Though the Naive Double-Submit Cookie method is simple and scalable, it remains vulnerable to cookie injection attacks, especially when attackers control subdomains or network environments allowing them to plant or overwrite cookies. For instance, an attacker-controlled subdomain (e.g., via DNS takeover) could inject a matching cookie and thus forge a valid request token. [This resource](https://owasp.org/www-chapter-london/assets/slides/David_Johansson-Double_Defeat_of_Double-Submit_Cookie.pdf) details these vulnerabilities. Therefore, always prefer the _Signed Double-Submit Cookie_ pattern with session-bound HMAC tokens to mitigate these threats.

## Fetch Metadata headers

Fetch Metadata request headers provide extra context about how an HTTP request was made, and how the resource will be used, enabling servers to reject suspicious cross-site requests. Servers can use these headers — most importantly `Sec-Fetch-Site` — as a lightweight and reliable method to block obvious cross-site requests. See the [Fetch Metadata specification](https://www.w3.org/TR/fetch-metadata/) for details.

Although Fetch Metadata headers are relatively new compared to [token-based defenses](#token-based-mitigation), they provide a simple way to block cross-origin state-changing requests and on modern browsers—can serve as a primary CSRF mitigation. The main caveat is compatibility: some legacy browsers, non-browser clients and certain webviews may not send `Sec-Fetch-*` headers, so deployments should include tested fallback behavior and a careful rollout plan.

The Fetch Metadata request headers are:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this section be focused on the subset of Sec-Fetch-* headers that are used to protect against CSRF? (Just Sec-Fetch-Site and Origin/Host header validation, and link off to MDN or something "if you'd like to learn more about what other Sec-Fetch-* headers are available for other purposes, see..."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @artis3n, sorry for the wait on the review

If I understood your comment correctly, you’re suggesting narrowing this section down to just Sec-Fetch-Site and Origin/Host header validation. I agree this would simplify things, but my thinking was that it’s important to keep the broader context of Fetch Metadata headers, since they work as a family and the spec encourages using them together.

and link off to MDN or something

we already link out to the W3C spec for readers who want deeper detail.

Maybe you can draft a short suggestion of what you’d like this section to look like?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, there are examples of Sec-Fetch-Mode, Sec-Fetch-Dest, and Sec-Fetch-User further down in the section.

Because of that, I think it makes sense to keep the broader description here, so all the related information stays in one place and developers don’t need to jump between different resources to understand how the full set of headers works together.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense if we want to keep them co-located. I do think we should distinguish between the headers you need to use for CSRF protection vs. the ones you can use for additional checks. E.g. mode, dest, and user.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mode, dest, and user are still part of the overall Fetch Metadata–based CSRF protection strategy, the Sec-Fetch-Site header is just the primary signal we rely on. But I get your point, I’ll try to update the wording to make that distinction clearer.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that making it clear that sec-fetch-site is the primary signal would be important, so as to not overcomplicate/overwhelm people. The others can be included for reference/deep dive.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated


- Sec-Fetch-Site — indicates relationship between request initiator’s origin and it's target's origin: `same-origin`, `same-site`, `cross-site`, or `none`.
- Sec-Fetch-Mode — indicates the request's [mode](https://fetch.spec.whatwg.org/#concept-request-mode)(e.g., `navigate`, `no-cors`, `cors`, `same-origin`, and `websocket`), which allows to distinguish between requests originating from a user navigating between HTML pages, and requests to load images and other resources.
- Sec-Fetch-Dest — indicates the [destination](https://fetch.spec.whatwg.org/#concept-request-destination) for the requested resource (e.g., `document`, `image`, `script`, etc.).
- Sec-Fetch-User — present only for navigation requests initiated by user. When sent value is `?1`, meaning `true`.

If any of headers above contain values not listed in the specification, in order to support forward-compatibility, servers should ignore those headers.

### Ease of use

Unlike [synchronizer tokens](#synchronizer-token-pattern) or [double-submit patterns](#alternative-using-a-double-submit-cookie-pattern) — which require additional client/server coordination and are easy to misimplement — Fetch Metadata checks are straightforward to implement correctly. They typically require only a small amount of server-side logic (inspect Sec-Fetch-Site, optionally refine with Sec-Fetch-Mode/Sec-Fetch-Dest) and no client changes. That simplicity reduces complexity, making the approach attractive for many applications.

### Browser compatibility

Fetch Metadata request headers are supported in most modern browsers on both desktop and mobile (Chrome, Edge, Firefox, Safari 16.4+, and even in webviews on both iOS and Android). For compatibility detail, see the [browser support table](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-Fetch-Site#browser_compatibility).

If your project requires absolute, 100% client coverage, [CSRF tokens](#token-based-mitigation) remain the safest universal option.

### How to treat Fetch Metadata headers on the server-side
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend we update this section. Maintaining language agnosticity, I'd like to see the pattern outlined in https://words.filippo.io/csrf/#protecting-against-csrf-in-2025 personally.

  1. Validate Origin header against an allowlist (for the JS example, recommend configuring as a new Set([domains]) for an easy set.has(Origin) as this validation step
  2. Check if Sec-Fetch-Site header is present
    a. If present, allow request if value is same-origin or none, else deny
  3. If Sec-Fetch-Site and Origin headers are both missing, pass request through
  4. Validate Origin header against Host header and pass request if they match, else reject (do this at the end and not with initial Origin validation due to step 3 support of significantly legacy browsers pre-2020)

Also the pattern described, basically, in https://web.dev/articles/fetch-metadata#how_to_use_fetch_metadata_to_protect_against_cross-origin_attacks .

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I'm partial since it is the language I am usually in but would be nice to note with the examples that Go developers can follow the above pattern by just using https://pkg.go.dev/net/[email protected]#CrossOriginProtection in the standard library as of Go 1.25.

Perhaps similar to https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#javascript-automatically-including-csrf-tokens-as-an-ajax-request-header , a section for Go: Use net/http CrossOriginProtection middleware with a link out to those docs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR's aproach was actually based primarily on the approach outlined in https://web.dev/articles/fetch-metadata#how_to_use_fetch_metadata_to_protect_against_cross-origin_attacks, however, I intentionally changed the flow to make it deny-by-default, rather than the more allow-by-default used in both that article and Filippo’s blog.

Both external sources propose flows that can short-circuit early and allow requests without evaluating Fetch Metadata. For example:

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

In this example, the request never reaches any Fetch Metadata checks. That effectively makes the Origin header the primary defense.

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

I’m not fully comfortable relying on that assumption. My interpretation is that it’s based on the earlier point that older (pre-2020) browsers sometimes omitted the Origin header on POST. The wording could also be misread as “pre-2020 browsers can’t be affected by CSRF,” which I’m sure isn’t the intended meaning.

if as a community we agree that Fetch Metadata is the stronger and more reliable signal in modern browsers, then it seems logical to evaluate Sec-Fetch-Site first and use standard headers (Origin/Referer/Host) only as a fallback for legacy or non-compliant clients.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a section for Go: Use net/http CrossOriginProtection middleware with a link out to those docs.

Regarding your second suggestion, that’s a great idea. We should definitely add a Go section to the “CSRF Prevention in Modern Frameworks” area. I can write a short part for it and include a reference to the CrossOriginProtection middleware along with a link to the documentation.

Copy link

@nickchomey nickchomey Nov 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the blog post uses Origin first, the actual Go source code checks sec-fetch-site first:

I have to assume this concern was raised while implementing the feature, and thus appropriately uses origin as a fallback.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for bringing this up, I went straight for the blog.

As we discussed, we’ll definitely add a reference to the Go implementation in the Frameworks section at the end of the document. Do you feel the current “### How to treat Fetch Metadata headers on the server-side” section needs any updates? @nickchomey

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this as part of "### Use Built-In Or Existing CSRF Implementations for CSRF Protection"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems OK to me.


`Sec-Fetch-Site` is the most useful Fetch Metadata header for blocking CSRF-like cross-origin requests and should be the primary signal in a Fetch-Metadata-based policy. Use other Fetch Metadata headers (`Sec-Fetch-Mode`, `Sec-Fetch-Dest`, `Sec-Fetch-User`) to further refine or tailor policies to your application (for example, allowing navigate mode top-level requests or permitting specific Dest values for resource endpoints).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is navigate mode defined anywhere?

**Policy (high level)**

1. If `Sec-Fetch-Site` is present:
1.1. Treat cross-site as untrusted for state-changing actions. By default, reject non-safe methods (POST / PUT / PATCH / DELETE) when `Sec-Fetch-Site: cross-site`.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't really a comment about the doc, but a related question.

In wordpress, Ajax requests are often made with GET to make mutations. This is, of course, wrong in many ways, but does happen.

Though, wp encourages the use of their nonce mechanism for Ajax, and doesn't use fetch metadata at all. Still, I will likely put in some effort to see if sec fetch can replace the wp nonces in a drop-in manner (just turn off/ignore nonces if they're there and use sec fetch)

So, I wonder what can be done to handle state-changing GET requests?

Should anything be mentioned here for the benefit of other frameworks or sites that use GET incorrectly?


```JavaScript
const SAFE = new Set(['GET','HEAD','OPTIONS']);
const site = req.get('Sec-Fetch-Site'); // e.g. 'cross-site','same-site','same-origin','none'

if (site === 'cross-site' && !SAFE.has(req.method)) {
return false; // forbid this request
}
```

1.2. Allow `same-origin`. Treat `same-site` as allowed only if your threat model trusts sibling subdomains; otherwise handle `same-site` conservatively (for example, require additional validation).

```JavaScript
const trustSameSite = false; // set true only if you trust sibling subdomains

if (site === 'same-origin') {
return true;
} else if (site === 'same-site') {
// handle same-site separately so the subcondition is clearly scoped to same-site
if (!trustSameSite && !SAFE.has(req.method)) {
return false; // treat same-site as untrusted for state-changing methods
}
return true;
}
```

1.3. Allow none for user-driven top-level navigations (bookmarks, typed URLs, explicit form submits) where appropriate.

2. If `Sec-Fetch-*` headers are absent: choose a fallback based on risk and compatibility requirements:
2.1. Fail-safe (recommended for sensitive endpoints): treat absence as unknown and block the request.
2.2. Fail-open (compatibility-first): fallback to other security measure (CSRF tokens, validate Origin/Referer, and/or require additional validation).

3. Additionall options
3.1 To ensure that your site can still be linked from other sites, you have to allow simple (HTTP GET) top-level navigation.

```JavaScript
if (req.get('Sec-Fetch-Mode') === 'navigate' &&
req.method === 'GET' &&
req.get('Sec-Fetch-Dest') !== 'object' &&
req.get('Sec-Fetch-Dest') !== 'embed') {
return true; // Allow this request
}
```

3.2 Whitelist explicit cross-origin flows. If certain endpoints intentionally accept cross-origin requests (CORS JSON APIs, third-party integrations, webhooks), explicitly exempt those endpoints from the global Sec-Fetch deny policy and secure them with proper CORS configuration, authentication, and logging.

### Limitations and gotchas

- Not universal. Some older browsers, webviews, bots, and non-browser HTTP clients do not send Sec-Fetch-*. Do not assume presence on every request — implement fallbacks.
- May break legitimate cross-origin integrations. A global Sec-Fetch policy can unintentionally block legitimate CORS or third-party flows; plan explicit whitelisting.
- One limitation is that Fetch Metadata request headers are only sent to [potentially trustworthy URLs](https://www.w3.org/TR/secure-contexts/#is-url-trustworthy). This means the headers will generally be present for requests to origins whose scheme is `https`, `wss`, or `file`, and for `localhost` (hosts in the `127.0.0.0/8` or `::1/128` ranges). For the full rules and additional edge cases (the algorithm the user agent uses to decide trustworthiness), see the [W3C Secure Contexts spec](https://www.w3.org/TR/secure-contexts/#is-origin-trustworthy).

### Rollout & testing recommendations

- Include an appropriate `Vary` header, in order to ensure that caches handle the response appropriately. For example, `Vary: Accept-Encoding, Sec-Fetch-Site`. See more [Fetch Metadata specification](https://w3c.github.io/webappsec-fetch-metadata/#vary).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add any info on what the issue is here that is being addressed? It isn't obvious to me, and surely most others.

- Start in “log only” mode. Record requests that would be blocked and review for false positives before enforcing. This is the safest way to discover legitimate flows that need whitelisting.
- Monitor UA coverage. Track which user agents include `Sec-Fetch-*` and which don’t; ensure your fallback logic covers missing-header cases. Use metrics to decide when to enforce stricter policies.
- Document exceptions. Keep an explicit list of endpoints whitelisted for cross-origin access.

## Disallowing simple requests

When a `<form>` tag is used to submit data, it sends a ["simple" request](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests) that browsers do not designate as "to be preflighted". These "simple" requests introduce risk of CSRF because browsers permit them to be sent to any origin. If your application uses `<form>` tags to submit data anywhere in your client, you will still need to protect them with alternate approaches described in this document such as tokens.
Expand Down Expand Up @@ -368,89 +460,6 @@ Cookie prefixes [are supported by all major browsers](https://developer.mozilla.

See the [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Directives) and [IETF Draft](https://tools.ietf.org/html/draft-west-cookie-prefixes-05) for further information about cookie prefixes.

### Use Fetch Metadata headers to verify nature of the request

Fetch Metadata request headers provide extra context about how an HTTP request was made, and how the resource will be used, enabling servers to reject suspicious cross-site requests. Servers can use these headers — most importantly `Sec-Fetch-Site` — as a lightweight method to block obvious cross-site requests. See the [Fetch Metadata specification](https://www.w3.org/TR/fetch-metadata/) for details.

It is important to note that Fetch Metadata headers should be implemented as an additional layer defense in depth concept. This attribute should not replace a CSRF tokens (or equivalent framework protections).

The Fetch Metadata request headers are:

- Sec-Fetch-Site — indicates relationship between request initiator’s origin and it's target's origin: `same-origin`, `same-site`, `cross-site`, or `none`.
- Sec-Fetch-Mode — indicates the request's [mode](https://fetch.spec.whatwg.org/#concept-request-mode)(e.g., `navigate`, `no-cors`, `cors`, `same-origin`, and `websocket`), which allows to distinguish between requests originating from a user navigating between HTML pages, and requests to load images and other resources.
- Sec-Fetch-Dest — indicates the [destination](https://fetch.spec.whatwg.org/#concept-request-destination) for the requested resource (e.g., `document`, `image`, `script`, etc.).
- Sec-Fetch-User — present only for navigation requests initiated by user. When sent value is `?1`, meaning `true`.

If any of headers above contain values not listed in the specification, in order to support forward-compatibility, servers should ignore those headers.

Browser compatability: Fetch Metadata request headers are supported in most modern browsers on both desktop and mobile (Chrome, Edge, Firefox, Safari 16.4+, and even in webviews on both iOS and Android). For compatibility detail, see the [browser support table](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-Fetch-Site#browser_compatibility).

#### How to treat Fetch Metadata headers on the server-side

Sec-Fetch-Site is the most useful Fetch Metadata header for blocking CSRF-like cross-origin requests and should be the primary signal in a Fetch-Metadata-based policy. Use other Fetch Metadata headers (Sec-Fetch-Mode, Sec-Fetch-Dest, Sec-Fetch-User) to further refine or tailor policies to your application (for example, allowing navigate mode top-level requests or permitting specific Dest values for resource endpoints).
**Policy (high level)**

1. If Sec-Fetch-Site is present:
1.1. Treat cross-site as untrusted for state-changing actions. By default, reject non-safe methods (POST / PUT / PATCH / DELETE) when `Sec-Fetch-Site: cross-site`.

```JavaScript
const SAFE = new Set(['GET','HEAD','OPTIONS']);
const site = req.get('Sec-Fetch-Site'); // e.g. 'cross-site','same-site','same-origin','none'

if (site === 'cross-site' && !SAFE.has(req.method)) {
return false; // forbid this request
}
```

1.2. Allow `same-origin`. Treat `same-site` as allowed only if your threat model trusts sibling subdomains; otherwise handle `same-site` conservatively (for example, require additional validation).

```JavaScript
const trustSameSite = false; // set true only if you trust sibling subdomains

if (site === 'same-origin') {
return true;
} else if (site === 'same-site') {
// handle same-site separately so the subcondition is clearly scoped to same-site
if (!trustSameSite && !SAFE.has(req.method)) {
return false; // treat same-site as untrusted for state-changing methods
}
return true;
}
```

1.3. Allow none for user-driven top-level navigations (bookmarks, typed URLs, explicit form submits) where appropriate.

2. If Sec-Fetch-* headers are absent: choose a fallback based on risk and compatibility requirements:
2.1. Fail-safe (recommended for sensitive endpoints): treat absence as unknown and block the request.
2.2. Fail-open (compatibility-first): fallback to other security measure (CSRF tokens, validate Origin/Referer, and/or require additional validation).

3. Additionall options
3.1 To ensure that your site can still be linked from other sites, you have to allow simple (HTTP GET) top-level navigation.

```JavaScript
if (req.get('Sec-Fetch-Mode') === 'navigate' &&
req.method === 'GET' &&
req.get('Sec-Fetch-Dest') !== 'object' &&
req.get('Sec-Fetch-Dest') !== 'embed') {
return true; // Allow this request
}
```

3.2 Whitelist explicit cross-origin flows. If certain endpoints intentionally accept cross-origin requests (CORS JSON APIs, third-party integrations, webhooks), explicitly exempt those endpoints from the global Sec-Fetch deny policy and secure them with proper CORS configuration, authentication, and logging.

#### Limitations and gotchas

- Not universal. Some older browsers, webviews, bots, and non-browser HTTP clients do not send Sec-Fetch-*. Do not assume presence on every request — implement fallbacks.
- May break legitimate cross-origin integrations. A global Sec-Fetch policy can unintentionally block legitimate CORS or third-party flows; plan explicit whitelisting.
- One limitation is that Fetch Metadata request headers are only sent to [potentially trustworthy URLs](https://www.w3.org/TR/secure-contexts/#is-url-trustworthy). This means the headers will generally be present for requests to origins whose scheme is `https`, `wss`, or `file`, and for `localhost` (hosts in the `127.0.0.0/8` or `::1/128` ranges). For the full rules and additional edge cases (the algorithm the user agent uses to decide trustworthiness), see the [W3C Secure Contexts spec](https://www.w3.org/TR/secure-contexts/#is-origin-trustworthy).

#### Rollout & testing recommendations

- Include an appropriate Vary header [RFC9110], in order to ensure that caches handle the response appropriately. For example, `Vary: Accept-Encoding, Sec-Fetch-Site`. See more [Fetch Metadata specification](https://w3c.github.io/webappsec-fetch-metadata/#vary).
- Start in “log only” mode. Record requests that would be blocked and review for false positives before enforcing. This is the safest way to discover legitimate flows that need whitelisting.
- Monitor UA coverage. Track which user agents include Sec-Fetch-* and which don’t; ensure your fallback logic covers missing-header cases. Use metrics to decide when to enforce stricter policies.
- Document exceptions. Keep an explicit list of endpoints whitelisted for cross-origin access.

### User Interaction-Based CSRF Defense

While all the techniques referenced here do not require any user interaction, sometimes it's easier or more appropriate to involve the user in the transaction to prevent unauthorized operations (forged via CSRF or otherwise). The following are some examples of techniques that can act as strong CSRF defense when implemented correctly.
Expand Down