CORS headers are the HTTP response headers a server sends to tell a browser which cross-origin websites are allowed to read its responses, with Access-Control-Allow-Origin being the one that actually decides who gets in. Cross-Origin Resource Sharing exists because the browser's same-origin policy blocks cross-site reads by default, and CORS is the controlled, opt-in mechanism to relax that. The trouble is that the most common configuration mistakes turn a security control into a security hole.
Let us walk through what CORS headers do, and then the specific ways they get misconfigured.
What problem CORS actually solves
By default, a browser lets a page make cross-origin requests but blocks the JavaScript on that page from reading the response unless the responding server explicitly allows it. That default protects users: a malicious site cannot silently read your authenticated bank API just because your browser is logged in.
CORS is how a server says "this other origin is allowed to read my responses." It is enforced by the browser, not the server. That is the mental model people get backwards. The server sends headers describing its policy; the browser decides whether to hand the response to the calling script. A non-browser client (curl, a backend service) ignores CORS entirely, which is why CORS is not an authentication or authorization mechanism.
The headers that matter
The response headers a server uses to express its CORS policy:
Access-Control-Allow-Origin— the origin(s) permitted to read the response. Either a specific origin likehttps://app.example.comor the wildcard*.Access-Control-Allow-Methods— which HTTP methods are allowed for the actual request.Access-Control-Allow-Headers— which request headers the client may send.Access-Control-Allow-Credentials— whether the browser may send cookies and include the response when credentials are involved.Access-Control-Max-Age— how long the browser may cache the preflight result.
For requests that are not "simple" (anything with a custom header, or methods like PUT and DELETE), the browser first sends a preflight OPTIONS request. Your server must answer that preflight with the appropriate allow headers, or the real request never fires.
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 600
The dangerous misconfigurations
Three mistakes account for most CORS-related vulnerabilities.
Wildcard origin with credentials. The spec forbids combining Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true, and browsers reject it. So developers "fix" it by reflecting the request's Origin header back verbatim while also allowing credentials. That is far worse: it means any origin is trusted with authenticated responses. If your server does this, any website a victim visits can read their authenticated data from your API.
// Dangerous: reflects any origin AND allows credentials
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
Trusting a poorly written origin allowlist. Checking whether the origin merely "contains" your domain lets https://example.com.attacker.com or https://notexample.com slip through. Match the full origin against an explicit allowlist, exact string comparison, no substring checks.
Overly broad wildcards on internal APIs. A wildcard is fine for a truly public, unauthenticated, read-only API. It is a liability the moment the endpoint returns anything user-specific.
Configuring CORS safely
The safe pattern is boring, which is the point:
- Maintain an explicit allowlist of origins you trust.
- On each request, compare the incoming
Originagainst that list with exact equality. - Only if it matches, echo that specific origin back in
Access-Control-Allow-Origin(never the wildcard when credentials are in play). - Set
Access-Control-Allow-Credentials: trueonly if you genuinely need cookies sent cross-origin.
const allowed = new Set([
'https://app.example.com',
'https://admin.example.com',
]);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin && allowed.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
next();
});
The Vary: Origin header is easy to forget and important: it tells caches that the response depends on the request origin, so a CDN does not serve one origin's allowed response to another origin.
Remember what CORS is not
CORS relaxes the same-origin policy for browsers. It does not authenticate anyone, it does not stop server-to-server requests, and it does not protect an API that is genuinely exposed to the internet without auth. A misconfigured CORS policy is regularly flagged in DAST scans precisely because it is invisible in normal testing and dangerous in production; our DAST product checks for the reflected-origin and wildcard-with-credentials patterns automatically. If you want the background theory, the Academy covers the same-origin policy in more depth.
FAQ
Do CORS headers protect my API from attackers?
No. CORS headers are enforced by browsers to control which web pages can read cross-origin responses. They do nothing against non-browser clients like curl or a malicious backend. Authentication and authorization are separate concerns you must implement independently.
Why does my request work in Postman but fail in the browser?
Postman and other non-browser tools do not enforce CORS, so they never send a preflight or check the allow headers. The browser does. A CORS error in the browser means your server is not returning the right Access-Control-Allow-Origin (and related) headers for that origin.
Is Access-Control-Allow-Origin: * always insecure?
Not for a public, unauthenticated, read-only endpoint. It becomes dangerous the moment the response contains user-specific or authenticated data, or if you try to combine it with credentials. For anything sensitive, use an explicit origin allowlist instead.
What is a CORS preflight request?
For requests that are not "simple", the browser first sends an OPTIONS request asking whether the real request is allowed. Your server answers with Access-Control-Allow-Methods and Access-Control-Allow-Headers. If the preflight is not answered correctly, the browser blocks the actual request.