CORS: What It Protects and Common Misconfigurations
Understand the same-origin policy, preflight requests, and the CORS mistakes that silently expose your API to arbitrary origins and attackers.
Netbay Developer Relations
Netbay Engineering
On this page
Cross-Origin Resource Sharing (CORS) is the most misunderstood header on the web. It does not block requests — browsers enforce it — and getting it wrong in either direction causes either silent breakage or a security hole. This post explains what CORS actually protects and the misconfigurations that ship regret.
The same-origin policy first
By default a browser will not let page scripts from origin A read responses from origin B. That is the same-origin policy, and browsers add CORS to *relax* it in controlled ways. A server opts into cross-origin reads by returning access-control headers. Without the exact right header, the browser blocks the page from reading the response even though the request reached the server.
The key mismatch: the **request arrives** at your server either way. CORS does not stop the call; it stops the *response* from being read by the foreign page. That is why CORS is a client-side enforcement mechanism, and why it must never be your only server-side safeguard — an attacker can make the same call with curl and read the result regardless.
Preflight and simple requests
Requests that would trigger side effects or use non-simple headers trigger a **preflight**: the browser first issues an 'OPTIONS' request and only proceeds if the server answers with the correct access-control headers. Simple requests (form-like GET/POST with basic content types) are sent directly but still need the allow-origin header for the browser to read the response.
Misconfiguration in preflight is a top cause of "it works in curl but not the browser" bugs — so always test with the exact Content-Type and headers your client sends.
A common source of confusion is credentialed requests. When a fetch includes cookies (the credentials mode is set to include), the server response must name an explicit origin — not a wildcard — and the allow-credentials header must be true. The reason is straightforward: a wildcard origin with credentials would let any site attach itself to your user's cookies, so browsers forbid the combination outright. If you see a mysterious browser error about credentials and a wildcard, that is the cause, and the fix is a concrete allow-list of origins that need cookies, kept entirely separate from any anonymous public endpoints.
Common misconfigurations
- **Wildcard with credentials.** 'Access-Control-Allow-Origin: *' combined with 'Allow-Credentials: true' is invalid and blocked by browsers; attackers exploit the confusion to bypass. Never mix them.
- **Reflecting any origin.** Reply with whatever 'Origin' the request sent. That makes every site a valid cross-origin reader of your API - effectively disabling the same-origin policy entirely.
- **Overly broad methods.** Allow only the methods you actually use, not '*'.
- **Long-lived preflight caches.** A too-large 'Access-Control-Max-Age' pins bad headers for hours and complicates changes.
A correct, narrow CORS config
This Fastify-style handler echoes only a configured list of origins and never a wildcard when credentials are involved.
const ALLOWED = [
"https://app.example.com",
"https://admin.example.com",
];
fastify.addHook("onRequest", (req, reply) => {
const origin = req.headers.origin;
const allow = origin && ALLOWED.includes(origin) ? origin : "";
if (allow) {
reply.header("Access-Control-Allow-Origin", allow);
reply.header("Vary", "Origin");
reply.header("Access-Control-Allow-Credentials", "true");
}
if (req.method === "OPTIONS") {
reply.header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE");
reply.header("Access-Control-Allow-Headers", "Content-Type,Authorization");
reply.header("Access-Control-Max-Age", "600");
return reply.code(204).send();
}
});Configuring allowed origins declaratively
cors:
allowedOrigins:
- https://app.example.com
- https://admin.example.com
allowedMethods: [GET, POST, PUT, DELETE]
allowCredentials: true
maxAgeSeconds: 600
allowedHeaders: [Content-Type, Authorization]Finally, verify what you ship. Point a browser at a test page on a second origin and watch the network tab for the preflight and the response headers, then intentionally set an origin that should be rejected and confirm the response is not readable. Small a mismatch list here — an unlisted subdomain, an extra header, a trailing slash — is exactly the kind of drift that quietly breaks or exposes an API weeks later.
Takeaway
CORS is a browser-side guard against cross-origin reads — it is not server-side security. Allow exactly the origins you trust, never a wildcard with credentials, and never reflect arbitrary origins. You can verify your headers against a fresh origin by deploying a test API and a test page on a Netbay VPS at netbayhosts.in.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance