Skip to content
CWE-346A07:2021 – Identification and Authentication Failures

Origin, Referer, and Fetch Metadata

Sec-Fetch-Site, Mode and Dest, Origin checks done correctly, the missing-header decision, and Resource Isolation Policy.

Headers the page cannot forge

The browser knows something the server does not: which page caused this request. It reports that in headers a page is forbidden from settingOrigin, Referer, and the Sec-Fetch-* family are all on the forbidden-header list, so no amount of fetch options or form markup can override them.

That makes them a genuine defence, and a cheap one. A single middleware rejecting cross-site state changes protects every route at once, with no tokens to mint, embed, store, or rotate.

The reason this is not the only defence is a chain of edge cases — proxies that strip headers, Origin: null, and the fact that a same-site attacker looks legitimate to all of them. Those are covered below and in Beating Referer and Origin Checks.

In 2026 the recommendation is: use Fetch Metadata as the primary header check, fall back to Origin, and keep a token underneath. Sec-Fetch-Site is supported in every current browser and is far easier to get right than Referer parsing.

The Sec-Fetch-* headers

HeaderValuesWhat it tells you
Sec-Fetch-Sitesame-origin, same-site, cross-site, noneThe relationship between the initiator and the target. `none` means the user initiated it directly — typed the URL, used a bookmark. This is the one that decides the CSRF question.
Sec-Fetch-Modenavigate, cors, no-cors, same-origin, websocketHow the request was made. `navigate` is a top-level navigation — including a cross-site form POST, the classic CSRF shape.
Sec-Fetch-Destdocument, image, script, style, iframe, empty, …What the result will be used as. An API endpoint receiving `Sec-Fetch-Dest: image` is being loaded by an <img> tag, which is never legitimate.
Sec-Fetch-User?1, or absentPresent only when a navigation was triggered by a genuine user gesture. Absent on a scripted auto-submit.

A Resource Isolation Policy

JavaScriptmiddleware.jsSecure
// The standard Resource Isolation Policy, applied before any route handler.
// Rejects cross-site state changes for the whole application at once.
function resourceIsolation(req, res, next) {
  const site = req.get('Sec-Fetch-Site')

  // Older browsers and non-browser clients send nothing. Allowing the request
  // through here is a deliberate choice: it keeps curl, mobile apps, and
  // server-to-server callers working. It is ALSO the bypass -- so this policy
  // must sit alongside a token, not replace it. If your clients are all
  // modern browsers, reject instead and the defence becomes complete.
  if (!site) return next()

  // same-origin: the app itself.  none: the user typed it or used a bookmark.
  if (site === 'same-origin' || site === 'none') return next()

  // same-site means a subdomain. Allow it only if you actually trust every
  // subdomain -- if any of them hosts user content or could be taken over,
  // treat this as cross-site. See the cookie-tossing guide.
  if (site === 'same-site') return next()

  // Everything left is cross-site. Safe methods are fine; anything that
  // changes state is not.
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next()

  return res.status(403).json({ error: 'cross-site request rejected' })
}

app.use(resourceIsolation)

Checking Origin correctly

JavaScriptVulnerable
function checkOrigin(req) {
  const origin = req.get('Origin')

  // Bug 1: a missing Origin passes. An attacker who can suppress the header
  // -- via a same-origin redirect chain, or on shapes that do not send it --
  // bypasses the check entirely.
  if (!origin) return true

  // Bug 2: substring matching. All of these pass:
  //   https://app.example.com.evil.com
  //   https://evil.com/?x=https://app.example.com
  //   https://notapp.example.com
  if (origin.includes('app.example.com')) return true

  // Bug 3 (the startsWith variant, equally common):
  //   https://app.example.com.evil.com  passes startsWith too
  return false
}

The Origin: null case

Origin: null is not a missing origin — it is a real value the browser sends from an opaque origin, and an attacker can produce it on demand. The situations that yield it:

  • A sandboxed iframe without allow-same-origin.
  • A data: URL.
  • A local file:// page.
  • Some cross-origin redirect chains.

So null must never appear in an allowlist. Treat it exactly like any other untrusted origin. The same warning applies to CORS: Access-Control-Allow-Origin: null with credentials is directly exploitable, because the attacker can make their page's origin be null.

The attacker's version is one attribute:

<iframe sandbox="allow-scripts allow-forms" srcdoc='
  <form id="p" action="https://app.example/account" method="POST">
    <input name="email" value="attacker@evil.example">
  </form>
  <script>document.getElementById("p").submit()</script>
'></iframe>

Note that the sandbox costs the attacker the cookie in most configurations — the request is a subresource navigation from an opaque origin — so this is primarily useful against SameSite=None targets and against non-cookie auth. See Beating Referer and Origin Checks.

Why Referer is the weakest of the three

Referer was the original header-based defence and it is the one to rely on least:

  • It is frequently absent. A Referrer-Policy of no-referrer suppresses it, users and privacy extensions strip it, and HTTPS-to-HTTP navigations drop it by default. An application that requires it will break for real users; an application that allows the missing case has an easy bypass.
  • It carries the full URL, so a token in a query string leaks to whatever the user navigates to next. That is a good reason never to put a token in a URL.
  • Its policy is partly attacker-controlled. The attacker's page sets its own Referrer-Policy, so it can choose to send a full referrer (unsafe-url) when that helps defeat a check.
  • Parsing it is error-prone. Every substring-matching bug in the vulnerable example above originated as Referer parsing.

If you must use it, parse it with a URL parser and compare .origin for exact equality. Prefer Origin, and prefer Sec-Fetch-Site over both.

Checklist

  • Prefer Sec-Fetch-Site. One header, four values, no parsing. Supported everywhere current.
  • Decide the missing-header case deliberately and write down why. Allowing it keeps non-browser clients working and is the documented bypass; denying it is the stronger position for a browser-facing route.
  • Exact-match full origins. Never includes, never startsWith, never a regex over a hostname.
  • Never allowlist null.
  • Decide whether same-site is trusted. If any subdomain hosts user content or could be taken over, it is not — see Subdomain Cookie Injection.
  • Keep a token underneath. Header checks fail open in exactly the cases you cannot enumerate; the token does not. Defense in Depth covers the layering order.