What is CORS (Cross-Origin Resource Sharing)?

Cross-Origin Resource Sharing (CORS) is an HTTP-header-based mechanism that lets a server declare which origins other than its own are permitted to load its resources. Without CORS, browsers enforce the same-origin policy, which blocks web pages from making requests to a different domain than the one that served the page. CORS relaxes that restriction in a controlled way, enabling legitimate cross-origin interactions while still protecting users from malicious sites.

If you have ever opened the browser console and seen Access to fetch at '...' has been blocked by CORS policy, you have encountered this mechanism first-hand. Understanding how it works — and how it fails — is essential for anyone building or securing web APIs.

The Same-Origin Policy

The same-origin policy (SOP) is the foundational browser security model. Two URLs share the same origin if and only if they have the same scheme, host, and port. Consider the page at https://app.example.com:443/dashboard:

SAME-ORIGIN COMPARISON Reference: https://app.example.com:443/dashboard https://app.example.com/page2 SAME path differs only http://app.example.com/dashboard CROSS scheme differs https://api.example.com/data CROSS host differs https://app.example.com:8443/api CROSS port differs https://example.com/dashboard CROSS host differs https://app.example.com:443/other SAME 443 is default

The key insight is that app.example.com and api.example.com are different origins, even though they share a parent domain. A subdomain change, a scheme change from HTTPS to HTTP, or a port change all produce a cross-origin request.

Why the Same-Origin Policy Exists

Without the SOP, any website you visit could silently read data from other sites where you are authenticated. Imagine visiting a malicious page that makes a fetch() call to your bank's API. If your browser automatically attaches cookies for the bank's domain and the malicious page can read the response, the attacker gets your account details. The SOP prevents this by blocking the malicious page from reading the response to that cross-origin request.

Importantly, the SOP does not block the request from being sent — it blocks the response from being read by the calling script. This distinction matters for understanding both CORS and CSRF (Cross-Site Request Forgery), which exploits the fact that requests are sent even when responses are blocked.

How CORS Works: Simple Requests

CORS defines two categories of cross-origin requests: simple requests and preflighted requests.

A request is "simple" if it meets all of the following conditions:

For a simple request, the browser sends the request directly with an Origin header indicating where the request came from. The server's response must include the Access-Control-Allow-Origin header matching the requesting origin (or * for any origin). If the header is missing or does not match, the browser blocks the response from reaching JavaScript — the network request completed, but the script cannot access the result.

GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json

{"status": "ok"}

Preflighted Requests

Any cross-origin request that does not qualify as "simple" triggers a preflight. Before the actual request, the browser sends an OPTIONS request asking the server whether the real request is permitted. This happens automatically and transparently — application code does not need to handle it.

Common triggers for preflight include using methods like PUT, DELETE, or PATCH; setting custom headers like Authorization or X-Request-ID; or sending a Content-Type of application/json.

CORS PREFLIGHT FLOW Browser Server JS calls fetch() with PUT OPTIONS /api/resource Origin: https://app.example.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: Authorization, Content-Type 204 No Content Access-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Methods: GET, PUT, DELETE Access-Control-Allow-Headers: Authorization, Content-Type Access-Control-Max-Age: 86400 Preflight passed PUT /api/resource Origin: https://app.example.com Authorization: Bearer eyJ... 200 OK Access-Control-Allow-Origin: https://app.example.com JS receives response

The preflight OPTIONS request includes two special headers: Access-Control-Request-Method (the HTTP method the real request will use) and Access-Control-Request-Headers (any non-safelisted headers the real request will include). The server's response to the preflight tells the browser whether to proceed.

If the server's preflight response does not include the right Access-Control-Allow-* headers, the browser never sends the actual request. The preflight acts as a gatekeeper.

CORS Response Headers

CORS is controlled entirely by response headers from the server. The server decides who gets access. The key headers are:

Access-Control-Allow-Origin

Specifies which origin is allowed. Can be a single origin (https://app.example.com) or * to allow any origin. You cannot list multiple origins in a single header value — the server must dynamically select one based on the incoming Origin header.

Access-Control-Allow-Methods

Lists the HTTP methods the server permits for cross-origin requests. Example: GET, POST, PUT, DELETE. Used only in preflight responses.

Access-Control-Allow-Headers

Lists the request headers the server permits beyond the CORS-safelisted set. Example: Authorization, Content-Type, X-Request-ID. Used only in preflight responses.

Access-Control-Expose-Headers

By default, cross-origin responses only expose a small set of "CORS-safelisted response headers" to JavaScript (Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma). If the server wants to expose additional headers like X-Request-ID or ETag, it must list them in this header.

Access-Control-Allow-Credentials

When set to true, the server allows the request to include credentials (cookies, HTTP authentication, client TLS certificates). This header has significant implications covered below.

Access-Control-Max-Age

Specifies how many seconds the browser can cache the preflight response. Without caching, every non-simple cross-origin request triggers two HTTP requests. A value of 86400 (24 hours) is common, though browsers cap this (Chrome at 2 hours, Firefox at 24 hours).

Credentialed Requests and Wildcard Restrictions

By default, cross-origin requests made with fetch() or XMLHttpRequest do not include cookies or other credentials. To include them, the client must opt in:

// fetch API
fetch('https://api.example.com/data', {
  credentials: 'include'
});

// XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.withCredentials = true;

When a request includes credentials, the CORS rules become stricter:

These restrictions exist because credentialed requests are the most security-sensitive type. A wildcard * combined with credentials would let any site on the internet make authenticated requests and read responses, effectively destroying the same-origin policy.

Common CORS Errors and Debugging

CORS errors are among the most frequently encountered issues in web development. Here are the most common scenarios and how to fix them.

Missing Access-Control-Allow-Origin

The server does not include the header at all. This is the most basic CORS error and usually means the server has no CORS configuration. Add the header to the server's responses for the relevant endpoints.

Origin Mismatch

The Access-Control-Allow-Origin header is present but does not match the requesting origin. Common causes: a trailing slash (https://example.com/ vs https://example.com), http vs https mismatch, or a port mismatch in development (localhost:3000 vs localhost:5173).

Preflight Fails

The server returns a 404, 405, or 500 for the OPTIONS request. Many server frameworks or reverse proxies do not handle OPTIONS by default. Ensure your server responds to OPTIONS requests on CORS-enabled endpoints.

Wildcard with Credentials

Using Access-Control-Allow-Origin: * when the request includes credentials. The browser rejects this combination. The server must echo the specific origin.

Missing Allowed Headers

The preflight response does not list a header the request includes. For example, sending Authorization but the server only allows Content-Type.

Debugging Tips

The browser's DevTools Network tab is the primary debugging tool. Look at both the preflight OPTIONS request and the actual request. Check the Origin header in the request and the Access-Control-Allow-* headers in the response. The Console tab will show the specific CORS error. Remember: the error is always on the server side — the browser is just enforcing what the server declared (or failed to declare).

CORS vs CSRF: Orthogonal Concerns

CORS and CSRF (Cross-Site Request Forgery) are frequently confused, but they address different threats.

CORS controls whether a cross-origin script can read a response. It does not prevent the request from being sent. A cross-origin POST with a simple content type will reach the server regardless of CORS headers — the browser just blocks the script from reading the response.

CSRF exploits the fact that browsers automatically attach cookies to requests. A malicious site can submit a form to your bank's transfer endpoint, and the browser will include your session cookie. The bank's server processes the transfer because the request looks legitimate. CSRF defenses (tokens, SameSite cookies, checking the Origin header) are needed independently of CORS.

CORS and CSRF protections are complementary. CORS prevents data exfiltration (reading responses), while CSRF tokens prevent unauthorized actions (state-changing requests). Neither replaces the other.

CORS and CDNs: The Vary Header

When a CDN caches a response, it needs to know whether the Access-Control-Allow-Origin header varies by request. If the server responds with Access-Control-Allow-Origin: https://app1.example.com for one request and that response is cached, a subsequent request from app2.example.com will receive the wrong origin in the header and fail.

The fix is the Vary header. The server must include Vary: Origin in responses so the CDN caches separate copies for each requesting origin:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app1.example.com
Vary: Origin
Content-Type: application/json

Without Vary: Origin, CDN caching can cause intermittent CORS failures that are extremely difficult to debug — requests fail or succeed depending on which origin's response the CDN has cached. This is a common pitfall when deploying APIs behind services like Cloudflare, CloudFront, or Fastly.

The same issue applies to preflight responses. If the CDN caches a preflight response, it must include Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers to ensure correct behavior.

CORS in APIs: Framework Configuration

Every major web framework provides CORS middleware or configuration. Here are examples for the most common ones.

Express (Node.js)

const cors = require('cors');
app.use(cors({
  origin: 'https://app.example.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Authorization', 'Content-Type'],
  credentials: true,
  maxAge: 86400
}));

Django (Python)

# settings.py (using django-cors-headers)
CORS_ALLOWED_ORIGINS = [
    "https://app.example.com",
]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = [
    "authorization",
    "content-type",
]

Spring Boot (Java)

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://app.example.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE")
            .allowedHeaders("Authorization", "Content-Type")
            .allowCredentials(true)
            .maxAge(86400);
    }
}

Go (net/http)

func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        origin := r.Header.Get("Origin")
        if origin == "https://app.example.com" {
            w.Header().Set("Access-Control-Allow-Origin", origin)
            w.Header().Set("Access-Control-Allow-Credentials", "true")
            w.Header().Set("Vary", "Origin")
        }
        if r.Method == "OPTIONS" {
            w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
            w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
            w.Header().Set("Access-Control-Max-Age", "86400")
            w.WriteHeader(204)
            return
        }
        next.ServeHTTP(w, r)
    })
}

A few principles apply regardless of framework: always validate the Origin header against an allowlist rather than reflecting it blindly, always include Vary: Origin when the response depends on the origin, and set Access-Control-Max-Age to reduce preflight overhead.

Private Network Access (CORS-RFC1918)

A newer extension to CORS called Private Network Access (originally known as CORS-RFC1918) adds protection for requests from public websites to private network resources. Without this protection, a malicious website loaded over the public internet could probe or attack devices on your local network — your router admin panel at 192.168.1.1, internal company services, or IoT devices.

Private Network Access introduces additional preflight requirements for requests from a "less private" context to a "more private" one:

The browser sends a preflight with the header Access-Control-Request-Private-Network: true, and the server must respond with Access-Control-Allow-Private-Network: true. This prevents drive-by attacks on local services that were never designed to be accessed from the web.

Chrome has been rolling out enforcement of Private Network Access since version 104. Developers of local services (development servers, IoT management interfaces, etc.) need to handle these preflight requests or the browser will block access from public web pages.

Security Pitfalls

CORS misconfigurations are a common source of security vulnerabilities. Understanding these pitfalls is as important as understanding how CORS works. For a broader view of web security, see our guides on XSS (Cross-Site Scripting) and how TLS/HTTPS works.

Reflecting the Origin Header

The most dangerous misconfiguration is blindly reflecting the Origin request header into the Access-Control-Allow-Origin response header:

// DANGEROUS: do not do this
w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))
w.Header().Set("Access-Control-Allow-Credentials", "true")

This effectively disables the same-origin policy entirely. Any website on the internet can make authenticated requests to your API and read the responses. It is functionally equivalent to having no CORS protection at all. Always validate the origin against an explicit allowlist.

The null Origin

Some servers are configured to allow the origin null. The null origin is sent by pages loaded from file:// URLs, sandboxed iframes, and certain redirect flows. Allowing null combined with credentials means an attacker can craft a sandboxed iframe that sends requests with an origin of null and read the responses:

<iframe sandbox="allow-scripts" src="data:text/html,
  <script>
    fetch('https://api.example.com/sensitive', {credentials:'include'})
      .then(r => r.text())
      .then(d => /* exfiltrate d */)
  </script>">
</iframe>

Never include null in your origin allowlist for production APIs.

Subdomain Wildcards and Regex Errors

Some implementations validate origins using poorly constructed regular expressions. A regex intended to match *.example.com might also match evil-example.com or example.com.evil.com if not anchored properly. Use exact string matching or a proper URL parser — never rely on .contains() or .endsWith() on raw strings without careful anchoring.

// DANGEROUS: matches attacker-example.com
if (origin.endsWith("example.com")) { ... }

// SAFE: proper domain validation
const url = new URL(origin);
if (url.hostname === "example.com" ||
    url.hostname.endsWith(".example.com")) { ... }

Overly Broad Wildcards

Using Access-Control-Allow-Origin: * is safe only for truly public, unauthenticated APIs. If your API relies on any form of authentication — API keys in headers, tokens, cookies — a wildcard origin is a misconfiguration. The browser's restriction against wildcards with credentials provides a safety net here, but many developers work around it incorrectly by reflecting origins.

Ignoring Preflight on the Server

Some developers set CORS headers only on their main endpoints but not on the OPTIONS handler. This causes preflighted requests to fail even though the actual endpoint is properly configured. Ensure that CORS middleware runs on all routes, including OPTIONS.

CORS and Server-to-Server Requests

CORS is exclusively a browser mechanism. When a backend server makes an HTTP request to another server, no CORS restrictions apply. There is no Origin header, no preflight, and no blocked responses. This is why CORS errors only appear in browser-based JavaScript and never in server-side code, curl commands, or mobile apps making direct HTTP requests.

This distinction frequently causes confusion: "It works in Postman but not in the browser." Postman, curl, and backend HTTP clients are not browsers and do not enforce the same-origin policy. If your API works from these tools but not from a browser, the issue is always a missing or incorrect CORS configuration on the server.

CORS Headers and HTTPS

CORS interacts with TLS/HTTPS through the origin comparison. An http:// origin is different from an https:// origin — they are separate origins even if the host and port are identical. Modern browsers also block mixed content: a page served over HTTPS cannot make unencrypted HTTP requests at all, regardless of CORS headers. This means cross-origin API endpoints should always be served over HTTPS.

Practical Recommendations

After understanding the mechanism, here are the practical guidelines for implementing CORS securely:

Summary

CORS is the mechanism that makes the modern web work across origins — it enables SPAs to call APIs on different domains, CDNs to serve resources to any site, and microservices to communicate through browsers. But it is also a mechanism that is routinely misconfigured. The key points to remember: CORS is a server-side declaration enforced by the browser; preflight requests gate non-simple cross-origin requests; credentialed requests demand specific origins, not wildcards; and reflecting the Origin header without validation is equivalent to having no CORS policy at all. Master these principles and most CORS-related headaches disappear.

See BGP routing data in real time

Open Looking Glass
More Articles
How TLS/HTTPS Works: Securing the Internet's Traffic
Certificate Transparency: How CT Logs Secure the Web's PKI
How Firewalls Work: Packet Filtering, Stateful Inspection, and Beyond
What is Cross-Site Scripting (XSS)?
What is Cross-Site Request Forgery (CSRF)?
What is Server-Side Request Forgery (SSRF)?