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

Beating Referer and Origin Checks

Absent-header fallbacks, startsWith and contains matching, Origin: null from a sandboxed frame, and unsafe-url to restore a full Referer.

Two questions to answer first

Header-based CSRF defence fails in two ways, and it is worth establishing which one you are looking at before trying anything else:

  1. What happens when the header is absent? If the check allows the request through, you only need to suppress the header. If it denies, you need to forge a value that passes.
  2. How is the value compared? Exact match against an allowlist is sound. includes, startsWith, endsWith, and hand-written regexes are all bypassable, usually trivially.

Test the first with a proxy: delete Referer, replay, see what happens. Then delete Origin. Then both. That is three requests and it tells you which half of this guide applies.

Note that a browser will not let you delete these headers from an attacker page — the techniques below are about making the browser omit them, which is a narrower set of options.

Suppressing the headers

TechniqueSuppressesNotes
<meta name="referrer" content="no-referrer">RefererThe attacker's page controls its own referrer policy. One tag, works everywhere. Does NOT affect Origin.
referrerpolicy="no-referrer" on the elementRefererPer-element version, for a specific form or link.
Redirect through a data: or blob: URLReferer and OriginThe intermediate has an opaque origin, so both are dropped or become null.
HTTPS to HTTP navigationRefererThe default referrer policy strips it on a downgrade. Only relevant if the target is reachable over plaintext.
Sandboxed iframe without allow-same-originOrigin becomes nullNot absent — a real value of `null`. See below.
GET navigation instead of a form POSTOriginOrigin is not sent on ordinary safe-method navigations. If the endpoint accepts GET, the Origin check has nothing to inspect.

The GET case is the easy win

This deserves its own note because it is so often overlooked. Browsers do not send Origin on a plain top-level GET navigation. It is sent on POST and on CORS requests, but a normal link click or location = ... carries no Origin header at all.

So an application whose sole CSRF defence is an Origin check, applied to a route that also accepts GET, has no defence on that route. The header the check depends on simply is not there, and the check's missing-header branch decides the outcome.

Combined with the fact that SameSite=Lax also permits top-level GET, this makes "does the endpoint accept GET?" the single highest-value question in CSRF testing. Ask it first.

Sec-Fetch-Site does not have this weakness — it is sent on every request including safe navigations, which is one of the main reasons to prefer it. See Origin, Referer, and Fetch Metadata.

Matching flaws

JavaScriptthe five classic mistakesVulnerable
// Target origin: https://app.example.com

// 1. includes() -- the attacker puts the string anywhere.
if (origin.includes('app.example.com')) allow()
//    https://app.example.com.evil.com        passes
//    https://evil.com/?r=https://app.example.com  passes (as a Referer)
//    https://notapp.example.com               passes

// 2. startsWith() -- suffix is unconstrained.
if (origin.startsWith('https://app.example.com')) allow()
//    https://app.example.com.evil.com        passes
//    https://app.example.com@evil.com        passes as a Referer (userinfo)

// 3. endsWith() -- prefix is unconstrained.
if (origin.endsWith('example.com')) allow()
//    https://evil-example.com                 passes
//    https://appexample.com                   passes

// 4. An unanchored or unescaped regex. The dots match any character and
//    there is no anchor at either end.
if (/app.example.com/.test(origin)) allow()
//    https://appXexampleYcom.evil.com        passes

// 5. Hostname extracted by splitting rather than parsing.
const host = origin.split('//')[1].split('/')[0]
if (host.endsWith('.example.com')) allow()
//    https://evil.com\\.example.com          may pass depending on the parser
//    Parser differentials between the check and the router are their own
//    class of bug -- always compare with a real URL parser.

// The fix is always the same: parse with new URL() and compare .origin for
// exact equality against an allowlist. An origin is an opaque string.

Origin: null

HTMLVulnerable
<!-- A sandboxed iframe without allow-same-origin has an OPAQUE origin, so
     the browser sends the literal string "null" as the Origin.

     This matters because "null" ends up on allowlists surprisingly often --
     developers see it in logs from local file:// testing and add it to make
     their own debugging work.

     Note the trade-off: the sandbox costs you the cookie in most cases,
     because the request is a subresource navigation from an opaque origin.
     This is primarily useful against SameSite=None targets, against
     non-cookie ambient auth, or where the check is the only defence and the
     cookie arrives by another route. -->
<iframe sandbox="allow-scripts allow-forms" srcdoc='
  <form id="poc" action="https://app.example.com/account/email" method="POST">
    <input type="hidden" name="email" value="attacker@evil.example" />
  </form>
  <script>document.getElementById("poc").submit()</script>
'></iframe>

Restoring a Referer with unsafe-url

The mirror image of suppression. If the check requires a Referer and denies when it is missing, the attacker's problem is that a modern default referrer policy may send only the origin, or nothing at all on a downgrade.

The attacker's page controls its own policy, so it can opt into sending the full URL:

<meta name="referrer" content="unsafe-url">

This matters when the check does substring matching on the path as well as the host. With unsafe-url the attacker can put the expected string into their own URL:

https://evil.example/csrf.html?https://app.example.com/account

A check doing referer.includes('app.example.com') now passes, because the target's origin appears in the attacker's query string. Combined with a permissive host match, this is the standard way a Referer-only defence falls.

An attacker can also host the payload at a path designed to satisfy the check — https://app.example.com.evil.example/ for a startsWith, or a subdomain named to satisfy an endsWith.

Proxies and infrastructure

Worth checking on any assessment with a CDN, WAF, or reverse proxy in front of the application, because the header the app sees may not be the header the browser sent:

  • Headers stripped in transit. Some proxies remove Origin or Referer for privacy or caching reasons. The application's missing-header branch then decides every request, and the team may not know it is being taken.
  • Headers rewritten. A proxy that normalises Origin to its own hostname makes every request look same-origin. This silently disables the defence entirely.
  • X-Forwarded-* trusted blindly. If the app derives its own origin from X-Forwarded-Host and compares it to Origin, and the proxy does not overwrite that header, an attacker sets both and the comparison passes.
  • Inconsistent normalisation. The WAF and the application may parse a URL differently — trailing dots, uppercase hosts, IDN, backslashes. A parser differential lets a request look same-origin to one and cross-origin to the other.

Test by sending the request with a spoofed X-Forwarded-Host and by observing, from a request-logging endpoint, exactly which headers survive the path from browser to application.

Prevention

  • Prefer Sec-Fetch-Site. Four fixed values, sent on every request including safe navigations, nothing to parse. No includes, no regex, no null case.
  • Exact-match full origins against an allowlist, parsed with a URL parser. Never substring-match, never build the comparison from string operations.
  • Never allowlist null. If local development needs an exception, gate it behind an environment flag that cannot be set in production.
  • Decide the missing-header case explicitly, write down the reason, and prefer default-deny for browser-facing state changes.
  • Do not derive your own origin from a request header unless the proxy is known to overwrite it.
  • Do not rely on Referer as the primary signal. It is absent too often to require and too malleable to trust.
  • Keep a token underneath. Every technique on this page is defeated by a synchronizer token, because none of them lets the attacker read the target's HTML.