API Security · CORS · Web Development · HTTP Headers · Browser Security · Same-Origin Policy
CORS Explained for Developers: Preflight Requests, Wildcards, and Safe Configuration
A practical guide to Cross-Origin Resource Sharing for indie developers and small teams. Learn how preflight requests work, why Access-Control-Allow-Origin wildcards break with credentials, and how to configure CORS without opening your API to abuse.
Published:
The Short Answer
CORS (Cross-Origin Resource Sharing) is a browser-enforced security mechanism that lets servers tell browsers which origins are allowed to read their responses. When your frontend JavaScript makes a request to a different origin, the browser checks CORS headers before passing the response back to your code. Misconfiguring these headers—especially using wildcards with credentials—is one of the most common and dangerous API security mistakes indie developers make.
What CORS Actually Is
The same-origin policy is a foundational browser security rule: scripts loaded from one origin cannot read responses from another origin unless explicitly permitted. An origin is defined by three components—scheme, host, and port. https://api.example.com:443 and https://api.example.com:8443 are different origins. https://api.example.com and http://api.example.com are different origins. Even https://api.example.com and https://www.api.example.com are different origins because the host differs.
CORS is the mechanism that relaxes this restriction in a controlled way. It works through HTTP headers. When your server responds to a cross-origin request, it includes headers like Access-Control-Allow-Origin to tell the browser whether the response can be read by the requesting script. Without these headers, the browser blocks the response and your JavaScript receives an error.
This is not an authentication or authorization mechanism. CORS controls whether the browser exposes the response to your code—it does not prevent direct HTTP requests from tools like curl or Postman. If your API returns sensitive data without proper authentication, CORS misconfiguration will not save you.
How Preflight Requests Work
Not every cross-origin request triggers extra traffic. Simple requests—typically GET or POST with standard content types like application/x-www-form-urlencoded, multipart/form-data, or text/plain—proceed directly. The browser sends the actual request and checks the response headers.
But requests that use methods like PUT, DELETE, or PATCH, or that send custom headers, or that use non-standard content types, trigger a preflight. The browser sends an OPTIONS request to the target origin first, asking the server which methods and headers are permitted. Only after the server responds with approval does the browser send the actual request.
You will see this in your browser’s network tab as two requests: first an OPTIONS request, then the actual GET, POST, or whatever method you intended. The preflight response includes headers like Access-Control-Allow-Methods and Access-Control-Allow-Headers to tell the browser what it is allowed to do.
This two-step process exists to prevent malicious scripts from forcing your browser to make destructive or sensitive requests to APIs you interact with daily. If a malicious page could trigger a DELETE request to your banking API without the server explicitly allowing it, the damage would be severe. The preflight is the browser’s way of asking permission first.
The Wildcard Trap: Why * Breaks with Credentials
This is where most developers make critical mistakes. The Access-Control-Allow-Origin header accepts either a specific origin like https://app.example.com or the wildcard * to allow any origin.
Here is the rule you must remember: you cannot use Access-Control-Allow-Origin: * when your API requires credentials. Credentials include cookies, HTTP authentication, and client-side TLS certificates. If your frontend sends credentials with requests to your API—and many do, especially for authenticated sessions—setting the allow-origin header to * will cause the browser to reject the response entirely, even though the request succeeds server-side.
Some developers try to work around this by reflecting the Origin header back as the value of Access-Control-Allow-Origin. This seems elegant: the server accepts requests from any origin and echoes it back. But this approach is dangerous when credentials are involved. An attacker can host a malicious page on evil.com that makes credentialed requests to your API. Your server reflects evil.com back as the allowed origin, and the browser grants the malicious page access to your API’s responses, including any cookies or authentication tokens. This is a textbook CORS misconfiguration vulnerability, classified under OWASP’s Security Misconfiguration category.
The correct approach is binary and deliberate:
- If your API is public and does not handle credentials, set
Access-Control-Allow-Origin: *and omitAccess-Control-Allow-Credentialsentirely. - If your API requires credentials, set
Access-Control-Allow-Originto the exact origin(s) that need access, and includeAccess-Control-Allow-Credentials: true. - Never reflect the
Originheader when credentials are in play.
Configuring CORS Safely: A Practical Checklist
1. Scope your CORS headers to API endpoints only. If your server serves both a website and an API, do not add CORS headers to every response. Only the API endpoints that need cross-origin access should return these headers. Adding them to static assets or internal pages creates unnecessary attack surface.
2. Use explicit origins, not wildcards, for authenticated APIs. Maintain a list of allowed origins in your configuration. If you have multiple frontend applications—perhaps a dashboard at https://dashboard.example.com and a mobile app backend at https://app.example.com—list each one explicitly. This is more work than *, but it is the only safe approach for credentialed access.
3. Handle preflight requests correctly. Your server must respond to OPTIONS requests with the appropriate headers. If you are using a framework, check whether it handles preflight automatically or whether you need to add middleware. Missing preflight responses will cause your frontend to fail with confusing errors that look like network problems but are actually CORS rejections.
4. Set Access-Control-Allow-Headers precisely. If your frontend sends custom headers like X-Request-ID or Authorization, the preflight response must include those headers in Access-Control-Allow-Headers. If you use * here, some browsers will reject it when credentials are involved. List the specific headers your API expects.
5. Use Access-Control-Max-Age to reduce preflight overhead. Preflight requests add latency. If your API consistently accepts the same methods and headers, you can tell browsers to cache the preflight response for a period of time using Access-Control-Max-Age. This reduces the number of OPTIONS requests without sacrificing security.
6. Never rely on CORS for access control. CORS is a browser mechanism, not a server security control. A determined attacker can bypass CORS entirely by making requests from server-side code, command-line tools, or custom clients. Always implement proper authentication and authorization on your API endpoints independently of CORS configuration.
Common CORS Misconfiguration Patterns and Their Risks
Reflecting the Origin header. As discussed, this allows any origin to make credentialed requests. The risk is credential theft and unauthorized data access from malicious third-party pages.
Overly broad Access-Control-Allow-Methods. Allowing all HTTP methods when your API only needs GET and POST expands the attack surface. An attacker could use DELETE or PATCH operations if your CORS configuration permits them.
Missing preflight handling. Some frameworks return 405 Method Not Allowed for OPTIONS requests. This silently breaks cross-origin requests that require preflight, causing frustration for frontend developers who may resort to unsafe workarounds like CORS browser extensions or server-side proxies.
CORS headers on non-API endpoints. Returning CORS headers on every response, including static pages and internal routes, violates the principle of least privilege. It gives every origin the ability to read responses from endpoints that were never intended for cross-origin access.
FAQ
Does CORS protect my API from unauthorized access? No. CORS only controls what the browser exposes to JavaScript. It does not authenticate users or authorize requests. Anyone can make direct HTTP requests to your API without going through a browser. Always implement authentication and authorization separately.
Why do I get CORS errors even though my API works in Postman? Postman does not enforce the same-origin policy. CORS is a browser security feature, not an API restriction. Your API may be perfectly functional; the browser is simply blocking the response because the CORS headers are missing or incorrect.
Can I use a wildcard for Access-Control-Allow-Headers?
In some cases, yes. But when Access-Control-Allow-Credentials is set to true, certain browsers enforce stricter rules. It is safer to list the specific headers your API requires rather than relying on wildcard behavior that may vary across browser implementations.
What is the difference between CORS and CSRF? CORS and CSRF are often confused but address different problems. CORS controls cross-origin read access from browser scripts. CSRF exploits authenticated sessions by tricking a user’s browser into making unintended requests. Proper CORS configuration does not prevent CSRF; you need separate CSRF protections like anti-forgery tokens.
Should I disable CORS entirely for development? Disabling CORS with browser extensions or flags is a development convenience, not a solution. It masks configuration problems that will surface in production. Fix the headers correctly rather than working around them.
Bottom Line
CORS is a necessary mechanism for modern web development, but it is easy to misconfigure. The core principle is simplicity and deliberation: allow only the origins that need access, be explicit about credentials, handle preflight requests correctly, and never treat CORS as a security control. Your API’s real security comes from authentication, authorization, input validation, and rate limiting—not from HTTP headers that browsers choose to enforce.
