CORS Explained:How Cross-Origin Requests Work
CORS is one of the most misunderstood parts of web security. Developers meet it as an error in the browser console, reach for the quickest fix that makes the error disappear, and often turn a security control into a hole. This guide explains what CORS is actually doing, why the browser blocks cross-origin reads in the first place, and how to configure it so your API is reachable by the front ends you trust and no one else.
What Is the Same-Origin Policy?
To understand CORS you first need the rule it relaxes: the same-origin policy. An origin is the combination of scheme, host, and port, for example https://app.example.com. The same-origin policy says that JavaScript running on one origin cannot read responses from a different origin. This is a core browser protection. Without it, a malicious page you visit could quietly call your bank's API using your logged-in session and read your balance. The same-origin policy stops that by blocking the read.
The important detail is what it blocks. The browser may still send the request, but it hides the response from the calling script unless the target server says sharing is allowed. CORS is the mechanism the server uses to say exactly that.
How Does CORS Work?
When a page on origin A makes a cross-origin request to server B, the browser adds an Origin header naming A. Server B decides whether to allow it and answers with CORS response headers. If the server returns an Access-Control-Allow-Origin that matches the caller, the browser hands the response to the script. If it does not, the browser blocks the read and you see the familiar CORS error, even though the server may have processed the request.
| Response Header | What It Controls |
|---|---|
| Access-Control-Allow-Origin | Which origin is allowed to read the response. Either one exact origin or *. |
| Access-Control-Allow-Methods | Which HTTP methods (GET, POST, PUT, DELETE) are permitted. |
| Access-Control-Allow-Headers | Which custom request headers the client may send. |
| Access-Control-Allow-Credentials | Whether cookies and auth headers may be included. Cannot be used with *. |
| Access-Control-Max-Age | How long the browser may cache the preflight result. |
What Is a CORS Preflight Request?
For anything beyond a simple request, the browser sends a preflight first: an automatic OPTIONS request that asks the server for permission before sending the real one. A request is not simple if it uses a method like PUT or DELETE, sends custom headers, or uses certain content types. The preflight carries Access-Control-Request-Method and Access-Control-Request-Headers, and the server must answer with matching allow headers. Only if the preflight passes does the browser send the actual request. This is why you sometimes see two requests in the network tab for a single API call.
# Browser preflight (sent automatically)
OPTIONS /api/orders
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
# Server response that allows it
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Credentials: true
What Are the Most Dangerous CORS Mistakes?
CORS misconfiguration is a real vulnerability class, not just a developer annoyance. The dangerous patterns all come from opening sharing too wide.
| Mistake | Why It Is Dangerous |
|---|---|
| Reflecting the Origin header back with credentials | Any site becomes a trusted origin, so an attacker page can read a logged-in user's private data. |
| Using Access-Control-Allow-Origin: * on private APIs | Exposes responses to every website. Acceptable only for truly public data. |
| Allowing credentials with a wildcard-like config | Combines an open origin policy with cookies, the exact recipe for data theft. |
| Trusting null as an origin | The null origin can be forced by sandboxed frames and used to bypass your allowlist. |
The worst of these is reflecting the caller's Origin back while also setting Access-Control-Allow-Credentials: true. Frameworks make this easy to do by accident when you try to support many front ends. It effectively means every website is allowed to make authenticated requests and read the results, which lets a malicious page steal data from any user who visits it while logged in to your app.
Does CORS Protect Against CSRF?
This is a common trap. CORS controls who can read a cross-origin response, but it does not stop the request from being sent. Cross-site request forgery abuses the fact that the browser sends cookies with requests the user never intended, and many CSRF attacks do not need to read the response at all: causing the state change is enough. So CORS is not a CSRF defence. You still need anti-CSRF tokens or the SameSite cookie attribute. The two controls solve different problems. For the full picture, read cross-site request forgery.
How Do You Configure CORS Safely?
- 1Keep an explicit allowlist of trusted origins and compare the incoming Origin against it exactly.
- 2Never reflect an arbitrary Origin header back into Access-Control-Allow-Origin.
- 3Only set Access-Control-Allow-Credentials: true for origins you fully control and trust.
- 4Never combine credentials with a wildcard. Browsers forbid it, and workarounds recreate the risk.
- 5Use Access-Control-Allow-Origin: * only for genuinely public, non-credentialed data.
- 6Allow the minimum set of methods and headers your front end actually needs, not a blanket list.
- 7Do not treat the null origin as trusted.
- 8Remember CORS is not a CSRF defence: keep SameSite cookies and anti-CSRF tokens in place.
What Does OWASP Say About CORS?
OWASP treats overly permissive CORS as a security misconfiguration and covers it in the HTML5 Security Cheat Sheet and the ASVS configuration controls. The guidance is to validate the Origin against a strict allowlist rather than reflecting it, to avoid the wildcard on any endpoint that returns private or credentialed data, and to never pair credentials with an open origin policy. OWASP also stresses that CORS complements, but never replaces, server-side authorisation: every request must still be authenticated and authorised on its own, because a determined client can send requests regardless of CORS.