CORS: The Browser's Cross-Origin Gate
CORS: The Browser's Cross-Origin Gate
CORS is the browser's security mechanism that controls which web pages can request resources from other origins. It is not a firewall. It is not server-side authentication. It is the browser deciding, on the user's behalf, whether to expose a cross-origin response to the page that asked for it.
What It Is
CORS is a browser-enforced access-control protocol. A web server declares, via HTTP headers, which origins may read its responses. The browser reads those headers and either hands the response to the requesting page or blocks it with a network error. Every cross-origin request the browser makes — fetch, XMLHttpRequest, WebSocket, fonts, images in canvas — is subject to this gate unless the request qualifies as a "simple" request that the server has already allowed.
Why It Matters
The web runs on the same-origin policy: a script from bank.com cannot read responses from evil.com. This is the foundation of web security. Without it, any malicious page you open could read your bank data, steal your session cookies, and act on your behalf.
CORS is the escape hatch. It lets a server deliberately relax the same-origin policy for specific origins, methods, and headers. It is the web's answer to the question: "How do we share data across origins without abandoning security entirely?"
The practical stakes are enormous. APIs, CDNs, microservices, authentication providers, payment gateways — all of them rely on CORS to function across domain boundaries. A misconfigured CORS policy is not a minor bug. It is an open door or a slammed gate, depending on which direction you err.
How It Works
The browser classifies every cross-origin request into one of two categories: simple requests or preflighted requests.
A simple request uses one of these methods: GET, HEAD, or POST. Its headers are limited to the CORS-safelisted set (Accept, Accept-Language, Content-Language, Content-Type with specific values). It triggers no preflight. The browser sends the request, reads the response headers, and either delivers the response or blocks it.
A preflighted request uses any other method (PUT, DELETE, PATCH), any custom header, or any Content-Type outside the safelisted values. Before the real request, the browser sends an OPTIONS request — the preflight — to the target origin. The server responds with Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers. The browser checks these. If the origin, method, and headers are all permitted, the browser sends the actual request. If not, the browser aborts. The requesting JavaScript sees only a generic network error. No status code. No body. Nothing.
The server must echo the requesting origin in Access-Control-Allow-Origin, or use for public resources. For credentials (cookies, HTTP auth, client certs), the server must send Access-Control-Allow-Credentials: true and the origin must be explicit. with credentials is forbidden.
Credentials are a footgun. If you send Access-Control-Allow-Credentials: true with Access-Control-Allow-Origin: *, the browser rejects the response. The origin must be explicit.
The Contract
The exact interface is a set of HTTP response headers. The browser reads them. The server sets them. No negotiation. No handshake beyond the preflight.
| Header | Purpose | Example | |--------|---------|---------| | Access-Control-Allow-Origin | Permitted origin(s) | https://client.com or * | | Access-Control-Allow-Methods | Permitted HTTP methods | GET, POST, PUT, DELETE | | Access-Control-Allow-Headers | Permitted custom headers | Content-Type, X-Auth-Token | | Access-Control-Allow-Credentials | Allow cookies/auth | true (must be exact) | | Access-Control-Expose-Headers | Headers the page may read | X-Total-Count, X-Rate-Limit | | Access-Control-Max-Age | Preflight cache duration | 86400 (seconds) |
The browser's contract is equally strict. If the response headers do not match the request, the response is discarded. The JavaScript caller receives no information about why. The browser's console may log the reason, but the code does not. This is deliberate: information leakage is also a security risk.
Real Examples
1. The API Gateway A REST API at api.example.com serves https://app.example.com. The API responds with Access-Control-Allow-Origin: https://app.example.com. No other origin is permitted. The browser on evil.com sends a request, gets the response, but the browser blocks it from the page. The data never reaches the attacker.
2. The CDN with Public Assets A CDN at cdn.example.com hosts images and fonts. It sends Access-Control-Allow-Origin: . Any page can load these assets. But no page can send credentials to fetch them. The wildcard and credentials are mutually exclusive.
3. The Auth Token Exchange A client at app.example.com sends POST /login with Content-Type: application/json and X-Auth-Token header. The server must respond to the preflight with Access-Control-Allow-Headers: Content-Type, X-Auth-Token. If the server omits X-Auth-Token, the browser aborts the real request. The login fails. No error message reaches the client. The developer opens DevTools and finds the CORS error in the console.
4. The WebSocket Upgrade WebSocket connections are not subject to CORS preflight. The browser sends the upgrade request with an Origin header. The server checks the origin and either accepts or rejects the connection. This is not CORS, but it is the same-origin principle applied to a different protocol.
5. The Microservice Mesh A frontend at portal.example.com calls billing.example.com, inventory.example.com, and auth.example.com. Each service must set its own CORS headers. If one service forgets, the portal breaks for that endpoint. The failure is silent. The user sees a blank widget. The network tab shows a 200 OK that the browser threw away.
Common Mistakes
Reflecting the origin blindly. A server reads the Origin header and echoes it back unconditionally. This is not . It looks like security. But if the origin is null (from a local file, a sandboxed iframe, or a redirect), the server reflects null, and the browser treats null as a valid origin. Some implementations also reflect when the origin is missing, which is even worse. This is how misconfigured CORS becomes a vulnerability.
Sending credentials with . The browser rejects this combination. The developer adds credentials: 'include' to fetch, the server sends Access-Control-Allow-Origin: , and every request fails. The fix is to echo the exact origin and add Access-Control-Allow-Credentials: true.
Relying on the server for security. CORS is a browser mechanism. It does not stop a curl script, a server-to-server request, or any non-browser client from calling your API. If you need access control, implement it at the API layer. CORS is defense in depth, not the primary defense.
Caching preflight responses incorrectly. A CDN caches a preflight response with Access-Control-Allow-Methods: GET. A client later tries POST. The browser uses the cached preflight, finds POST is not allowed, and fails. The server supports POST. The CDN is wrong. Cache busting or proper Vary: Origin headers are the fix.
Thinking the preflight is optional. You cannot disable preflight. The browser decides. If your API requires custom headers, the preflight happens. You can reduce the cost with Access-Control-Max-Age, but you cannot eliminate it.
Connection to OIP
OIP is built on the principle that systems must be open, deterministic, and auditable. CORS is a poor approximation of this, but it shares the same underlying concern: who gets to read what, and under what conditions.
An OIP-compliant system does not hide access rules inside a browser's black-box enforcement. The rules are explicit: a directory row declares its inputs, its outputs, and its permissions. There is no silent failure. There is no request that returns 200 but delivers nothing to the caller because a header was misaligned.
CORS is a bridge between the old web and the OIP philosophy. It forces a server to declare its cross-origin policy in headers — a form of self-describing contract. The browser enforces that contract. The problem is opacity: the browser's decision is not auditable by the calling code, and the error is not actionable.
In an OIP system, every capability is a directory row with explicit inputs and outputs. The contract is visible. The enforcement is transparent. The failure is explainable. CORS is a step toward that world, but it is trapped in a model where the browser is the intermediary and the developer is the last to know what went wrong.
The lesson is this: CORS matters because cross-origin boundaries are real and dangerous. The way to get it right is to treat it as a formal contract — exact headers, exact origins, exact methods — and to verify it in practice, not in theory. That is the OIP way.
Connection to the Grain Philosophy
This protocol is part of the Open Inventory Protocol — a living system of self-describing voxels that serves the Grain philosophy. The OIP is the interface. The philosophy is the core.