Picture of Manick Bhan

CORS (Cross-Origin Resource Sharing): What Is It and How Does It Work?

Published on: May 28, 2026
Last updated: June 3, 2026

Did like a post? Share it with:

Picture of Manick Bhan

CORS (Cross-Origin Resource Sharing) is an HTTP header mechanism that browsers enforce to control which external origins receive permission to load resources from a server. Cross-origin resource sharing governs how web browsers handle requests to domains, schemes, and ports that differ from the page’s origin. The same-origin policy forms the baseline security rule that CORS extends. The same-origin policy prevents scripts from requesting resources outside their own origin. CORS provides a controlled pathway for legitimate cross-origin communication by requiring servers to declare which external origins they trust.

Modern web applications routinely cross origin boundaries. A front-end application on app.example.com requests data from api.example.com. A web page loads fonts from a content delivery network. An analytics script sends data back to a third-party tracking server. Each of those connections requires a cross-origin request. The browser blocks those connections by default. Cross-origin resource sharing defines the rules for when and how those connections proceed.

CORS operates through HTTP response headers. A server that accepts cross-origin requests returns specific headers that declare the trusted origins, permitted HTTP methods, and allowed request headers. The browser reads those headers. The browser exposes the response to the requesting script if the headers authorize it. The browser blocks access and logs an error if the headers are absent or do not match the requesting origin.

Server-side CORS configuration varies across Express, Nginx, and Apache. Each platform provides its own mechanism for setting response headers. The configuration method matches the request type. Credentialed requests and preflighted requests require specific header combinations that differ from simple request configurations.

What Is CORS?

CORS (Cross-Origin Resource Sharing) is an HTTP header mechanism that allows a server to declare which external origins a browser is permitted to load resources from. The declaration happens through the response headers the server returns. The browser reads those headers after receiving the response and decides whether to expose the response data to the requesting script. Cross-origin resource sharing extends the same-origin policy. The same-origin policy is the browser rule that restricts scripts from making HTTP requests to origins outside their own. An origin is defined by scheme (http or https), the hostname (example.com), and the port (80, 443, or a custom port). Two URLs share an origin only if all three components match exactly.

What counts as a cross-origin request? A request is cross-origin if the scheme, hostname, or port of the request URL differs from any of those components on the page origin. A page on https://app.example.com requesting data from https://api.example.com crosses an origin boundary because the hostnames differ. A page on http://example.com requesting from https://example.com crosses a boundary because the schemes differ. A page on https://example.com:3000 requesting from https://example.com:4000 crosses a boundary because the ports differ.

What does CORS permit? CORS permits servers to declare specific external origins as trusted for cross-origin requests. The server returns an Access-Control-Allow-Origin header containing the trusted origin’s value. The browser compares the request’s Origin header against the Access-Control-Allow-Origin response header. The browser exposes the response to the script when the values match.

What does CORS restrict? CORS restricts browsers from exposing cross-origin responses to scripts by default. The restriction applies to responses from fetch() and XMLHttpRequest calls. The restriction does not apply to HTML elements that load cross-origin resources passively (img, script, link). Cross-origin image loading, script loading, and form submissions fall outside CORS governance. CORS governs programmatic access to cross-origin response bodies.

What is the requesting origin? The requesting origin is the scheme, hostname, and port of the page that contains the script making the request. The browser adds the Origin header automatically. Application code does not set the Origin header. The browser sets it on every cross-origin request.

What requests does the same-origin policy affect? The same-origin policy affects cross-origin fetch() and XMLHttpRequest calls. The policy does not block loading cross-origin images, CSS files, scripts, or fonts through HTML elements. The policy does not block cross-origin form submissions. The policy blocks scripts from reading the response of a cross-origin request. CORS relaxes that block for authorized origins.

Why CORS Is a Browser Enforcement Mechanism Rather Than a Server-Side Security Feature?

CORS is enforced by the browser, not the server. The server returns CORS headers, but the server does not enforce them. The browser enforces them. A request sent from Curl, Postman, a server-side HTTP client, or any context outside a web browser is not subject to CORS restrictions. Those requests receive the response regardless of the server’s CORS headers.

Why does the enforcement location matter? The enforcement location matters because it defines what CORS protects. CORS protects browser-based scripts from reading cross-origin responses without explicit server permission. CORS does not protect the server from receiving unauthorized requests. The server receives the request in all cases. The browser alone decides whether to expose the response to the script.

What does a CORS misconfiguration expose? A CORS misconfiguration exposes response data to scripts on unauthorized origins. The server still receives the request. The server still processes it. The misconfiguration allows scripts from the wrong origin to read the response. The risk is concrete for authenticated responses that contain user data, session tokens, or other sensitive information.

How does this differ from server-side authentication? Server-side authentication decides who receives a valid response. CORS decides which browser origins read that response. A server that requires authentication and has no CORS configuration blocks cross-origin script access to responses, even for authenticated users. A server with CORS configuration but no authentication accepts requests from allowed origins without verifying identity. Both mechanisms are independent and serve different purposes.

What is the implication for server hardening? Hardening a server against unauthorized access requires authentication and authorization controls, not CORS configuration. CORS configuration controls which browser origins read responses. A server that relies on CORS as its only access control is vulnerable to any non-browser HTTP client. Correctly designed APIs combine CORS with authentication.

Why Does CORS Matter?

CORS matters because modern web applications make cross-origin requests as a standard part of their operation. A single-page application hosted on one domain communicates with an API on another domain, loads fonts from a content delivery network, and sends data to analytics or error-tracking services. Each of those communications crosses an origin boundary. CORS defines whether those communications succeed.

What happens without CORS headers? Without CORS headers, browsers apply the same-origin policy to all cross-origin responses from fetch() and XMLHttpRequest. Scripts receive no data from those requests. The application fails at every cross-origin call. Cross-origin resource sharing provides the mechanism for servers to opt into cross-origin communication on a per-origin, per-method, and per-header basis.

Why does CORS exist as a separate mechanism? The same-origin policy predates modern web architecture. The same-origin policy was designed for a web where pages and their data lived on the same server. API-first architecture, microservices, and distributed content delivery changed that assumption. CORS was added to the web platform to handle cross-origin communication without removing the same-origin policy entirely.

Where does CORS appear in real-world applications? CORS appears in four main contexts in real applications. The four contexts are listed below.

  1. Front-end applications requesting data from a separate API domain.
  2. Web applications load fonts or static assets from a content delivery network.
  3. Third-party integrations (analytics, tracking, A/B testing) where external scripts send data back to their own servers.
  4. Authentication flows where a browser authenticates against a separate identity provider.

What does CORS configure in each context? In a front-end to API context, CORS configuration tells the browser which origins the API accepts requests from. In a content delivery network context, CORS configuration on the CDN allows font and asset loading from authorized web origins. In a third-party integration context, the third-party server configures CORS to accept requests from the pages that embed their scripts. In an authentication context, CORS configuration governs whether the browser sends and reads authentication cookies and tokens across origins.

How does a missing CORS configuration manifest? A missing CORS configuration produces a blocked request and a browser console error. The error reads “Access to fetch at [URL] from origin [origin] has been blocked by CORS policy. No ‘Access-Control-Allow-Origin’ header is present on the requested resource.” The application fails at the point of the cross-origin request. No data reaches the script. The server received the request and returned a response. The browser blocked access to it.

What is the performance cost of CORS preflight requests? Preflighted requests add one HTTP round-trip before each non-simple cross-origin request. A preflight to a server 100ms away adds 100ms to every request that triggers it. The Access-Control-Max-Age header caches preflight results. A cached preflight eliminates the extra round-trip for subsequent requests to the same endpoint within the cache window.

What Is the Difference Between CORS and JSONP?

CORS and JSONP are two different mechanisms for enabling cross-origin data access in the browser, but they operate through fundamentally different methods and carry different security profiles.

CORS uses HTTP headers to declare server permissions. The browser reads those headers and grants or denies script access to the response. JSONP (JSON with Padding) exploits the browser’s allowance of cross-origin script loading through the <script> tag. JSONP wraps a JSON response in a JavaScript function call and loads it as an external script. The browser executes that script, which calls a function defined on the requesting page.

What is the core technical difference? The core technical difference is the transport mechanism. CORS uses the standard HTTP request/response model with access control headers. JSONP uses dynamic script injection where the server returns executable JavaScript instead of data. CORS works with any HTTP method. JSONP works only with GET requests because only GET requests load through script tags.

What is the security difference? CORS requires the server to explicitly declare which origins receive access. The server controls the allowlist. JSONP requires no server-side origin verification. Any page on the web loads a JSONP endpoint by crafting a script tag. JSONP provides no mechanism for the server to restrict which origins execute the response.

The table below compares CORS and JSONP across the dimensions that matter for practical use.

DimensionCORSJSONP
MechanismHTTP response headersScript tag injection
HTTP methodsGET, POST, PUT, DELETE, PATCH, OPTIONSGET only
Request typesSimple and preflightedGET requests only
CredentialsSupported with explicit configurationNot supported
Error handlingStandard HTTP error codesNo standard mechanism
Server-side origin controlServer declares trusted origins through headersNo origin verification
Browser supportAll modern browsersLegacy; deprecated in modern use
SpecificationWHATWG Fetch specificationNo formal specification
StatusCurrent standardDeprecated

Why is JSONP deprecated? JSONP is deprecated because it requires the server to execute whatever callback function name the client provides in the query string. A malicious page loads a JSONP endpoint from any other site if that endpoint returns executable JavaScript. JSONP provides no mechanism for the server to verify the requesting origin. CORS replaced JSONP with a header-based approach that the server controls entirely.

What security problem does JSONP introduce? JSONP introduces the risk that any site loads data from a JSONP endpoint by crafting a script tag. The response executes as JavaScript on any page that loads it. A JSONP API endpoint that returns user data exposes that data to any page that knows the URL. CORS eliminates that exposure by requiring the server to declare trusted origins through Access-Control-Allow-Origin.

When does JSONP still appear? JSONP still appears in legacy codebases and old third-party integrations. Libraries written before CORS became universally supported use JSONP for cross-origin requests. Modern applications use CORS exclusively. A codebase that still uses JSONP carries a security risk that CORS does not.

What is the practical difference for developers? The practical difference is that CORS requires server-side configuration and JSONP requires a specific API response format. A developer migrating from JSONP to CORS adds Access-Control-Allow-Origin headers to the server, removes the callback wrapper from the API response, and updates the client code to use fetch() instead of dynamic script injection.

How Does CORS Work?

CORS works through an HTTP header exchange between the browser and the server, where the browser sends an Origin header with every cross-origin request and the server returns Access-Control headers that declare whether the request is permitted.

The exchange follows one of two paths depending on the request type. Simple requests proceed directly without a preliminary check. Preflighted requests require the browser to send an HTTP OPTIONS request first to verify that the server accepts the main request.

What determines whether a request is simple or preflighted? A request is simple if it meets all three conditions. The three conditions are listed below.

  1. The HTTP method is GET, POST, or HEAD.
  2. The Content-Type header, if present, is one of application/x-www-form-urlencoded, multipart/form-data, or text/plain.
  3. The request includes no custom headers beyond the CORS-safe list.

A request becomes preflighted if it uses any other HTTP method, sets Content-Type to application/json or text/xml, or adds any custom headers.

What triggers a preflight request in practice? Preflight requests are triggered by fetch() calls that use PUT, DELETE, or PATCH, fetch() calls with a Content-Type of application/json, fetch() calls that include an Authorization header, and fetch() calls that include any custom header. These are the most common triggers in production web applications.

What is the origin of the term “preflight”? The term preflight describes the OPTIONS request that flies ahead of the main request to check whether the main request is safe to send. The OPTIONS method was chosen because it is defined as a request for information about the communication options available at a URL. It does not modify the server state. It is safe to send it as a check before a state-modifying request.

How Browsers Send Cross-Origin Requests?

Browsers send cross-origin requests by automatically adding an Origin header that identifies the requesting page’s origin. The Origin header is added by the browser. Application code does not set the Origin header. The header value is the scheme, hostname, and port of the page making the request.

What does the Origin header contain? The Origin header contains the origin of the requesting page in the format scheme://hostname:port. A page on https://app.example.com sends Origin: https://app.example.com. The port is omitted for standard ports (80 for HTTP, 443 for HTTPS). A page on http://localhost:3000 sends Origin: http://localhost:3000.

How does the browser send a simple request? The browser sends a simple request directly to the target URL. The browser adds the Origin header. The server receives the request, processes it, and returns a response. The browser checks the response for an Access-Control-Allow-Origin header. The browser exposes the response to the script if the header is present and matches the requesting origin.

How does the browser send a preflighted request? The browser sends a preflighted request in two stages. Firstly, the browser sends an HTTP OPTIONS request to the same URL. The OPTIONS request includes the Origin header, an Access-Control-Request-Method header with the intended method, and an Access-Control-Request-Headers header listing any custom headers the main request will use. Secondly, the browser reads the OPTIONS response. Thirdly, the browser sends the main request only if the OPTIONS response confirms that the main request is permitted.

What does a preflight request look like? A preflight for a PUT request with a Content-Type of application/json is below.

OPTIONS /api/data HTTP/1.1Host: api.example.comOrigin: https://app.example.comAccess-Control-Request-Method: PUTAccess-Control-Request-Headers: Content-Type

The expected preflight response:

HTTP/1.1 204 No ContentAccess-Control-Allow-Origin: https://app.example.comAccess-Control-Allow-Methods: GET, PUT, POST, DELETEAccess-Control-Allow-Headers: Content-TypeAccess-Control-Max-Age: 86400

What happens after the preflight succeeds? The browser sends the main PUT request with the original headers. The server returns a response with at minimum an Access-Control-Allow-Origin header. The browser exposes the response to the script.

What happens if the preflight fails? The browser blocks the main request entirely. The browser logs a CORS error, identifying which header or value caused the failure. The server never receives the main request.

How Browsers Validate CORS Response Headers?

Browsers validate CORS response headers by comparing the Access-Control-Allow-Origin value against the Origin header of the request. The comparison is exact. The browser does not perform prefix matching, suffix matching, or subdomain matching. The browser checks only whether the Access-Control-Allow-Origin value equals the requesting origin exactly or equals the wildcard value.

What happens if the values match? The browser exposes the response body to the script. The script reads the response data. The request completes successfully from the script’s perspective.

What happens if the values do not match? The browser blocks access to the response. The browser logs a CORS error to the console. The script receives a network error. The response body is inaccessible to the script.

Does the server still receive the request even when CORS fails? The server receives the request in all cases. The server processes the request and returns a response. The browser inspects the response headers after receiving the full response. The browser then decides whether to expose the response to the script. The CORS check happens in the browser after the round-trip completes.

What headers does the browser check for preflighted requests? For preflighted requests, the browser checks four headers in the OPTIONS response. The four headers are listed below.

  1. Access-Control-Allow-Origin, match the requesting origin.
  2. Access-Control-Allow-Methods, list the intended HTTP method.
  3. Access-Control-Allow-Headers, list every custom header in the main request.
  4. Access-Control-Max-Age, optional; sets the preflight cache duration in seconds.

The browser proceeds with the main request only after all checks pass.

What CORS Headers Does a Server Return?

A server returns up to six CORS response headers that declare the permissions for cross-origin requests. The six headers are Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Credentials, Access-Control-Max-Age, and Access-Control-Expose-Headers. Not all six are required in every response. The required headers depend on the request type.

Which headers are required for simple requests? Simple requests require only Access-Control-Allow-Origin. The server returns this header with the value set to the requesting origin or to the wildcard *. The browser reads this header and decides whether to expose the response.

Which headers are required for preflighted requests? Preflighted requests require Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers in the OPTIONS response. Access-Control-Max-Age is optional but reduces preflight overhead for frequently repeated requests. The actual request response requires at a minimum Access-Control-Allow-Origin.

Which headers are required for credentialed requests? Credentialed requests require Access-Control-Allow-Origin set to an explicit origin (not *) and Access-Control-Allow-Credentials set to true. A credentialed request that receives * in Access-Control-Allow-Origin fails at the browser validation step regardless of all other headers.

What does Access-Control-Expose-Headers do? Access-Control-Expose-Headers tells the browser which response headers are accessible to JavaScript. By default, only six headers are readable by scripts (Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, and Pragma). Headers outside that list are hidden from scripts unless listed in Access-Control-Expose-Headers.

How does the server decide which headers to return? The server returns CORS headers based on the Origin header in the request. A correctly configured CORS implementation checks the incoming Origin value against an allowlist. The server returns headers matching the allowed origin. The server returns no CORS headers for origins not on the allowlist.

The table below summarizes which headers are required for each request type.

Request typeRequired headersOptional headers
Simple requestAccess-Control-Allow-OriginAccess-Control-Expose-Headers
Preflighted request (OPTIONS)Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-HeadersAccess-Control-Max-Age
Preflighted request (main)Access-Control-Allow-OriginAccess-Control-Expose-Headers
Credentialed requestAccess-Control-Allow-Origin (explicit), Access-Control-Allow-Credentials: trueAccess-Control-Expose-Headers

What Does Access-Control-Allow-Origin Do?

Access-Control-Allow-Origin is the primary CORS response header that tells the browser which origin is allowed to read the response. The browser compares the value of this header against the Origin header of the request. The browser grants access to the response if the values match.

Four additional headers work alongside Access-Control-Allow-Origin to complete the CORS configuration. The headers are listed below.

  1. Access-Control-Allow-Methods and Access-Control-Allow-Headers.
  2. Access-Control-Allow-Credentials and Credentialed Requests.
  3. Access-Control-Max-Age and Preflight Caching.
  4. Access-Control-Expose-Headers and Browser Access Rules.

What values does Access-Control-Allow-Origin accept? Access-Control-Allow-Origin accepts two types of values. The first is a specific origin (https://app.example.com). The second is the wildcard (*). The wildcard permits any origin to read the response. A specific origin permits only that one origin. The header does not accept a comma-separated list of multiple origins. A server that needs to allow multiple origins checks the incoming Origin header and returns the matching origin dynamically.

How does a server return a dynamic origin value? The server reads the Origin header from the incoming request. The server checks the value against an allowlist of permitted origins. The server sets the Access-Control-Allow-Origin response header to the incoming Origin value if it is on the allowlist. The server sets a Vary: Origin response header to indicate that the response varies by origin.

Why is the Vary header necessary? The Vary header is necessary because shared caches (CDNs, reverse proxies) cache responses. A response cached for origin A with Access-Control-Allow-Origin: https://a.example.com isn’t served to origin B. The Vary: Origin header instructs caches to store separate copies per origin value. Missing the Vary header causes incorrect CORS responses to be served from cache.

Access-Control-Allow-Methods and Access-Control-Allow-Headers 

Access-Control-Allow-Methods tells the browser which HTTP methods the server accepts in cross-origin requests from the allowed origin. The header appears in the response to the preflight OPTIONS request. The browser checks whether the intended method is listed before sending the main request.

What values does Access-Control-Allow-Methods accept? Access-Control-Allow-Methods accepts a comma-separated list of HTTP method names in uppercase. Common values are GET, POST, PUT, DELETE, PATCH, and OPTIONS. A server that accepts all standard methods returns Access-Control-Allow-Methods: GET, HEAD, POST, PUT, DELETE, PATCH, OPTIONS.

What happens if the intended method is not listed? The browser blocks the main request. The browser logs a CORS error stating the method is not permitted. The main request never reaches the server.

What does Access-Control-Allow-Headers do? Access-Control-Allow-Headers tells the browser which request headers the server accepts in cross-origin requests. The header appears in the response to the preflight OPTIONS request. The browser checks whether each custom header in the main request is listed before proceeding.

What headers require explicit listing? Headers that require explicit listing in Access-Control-Allow-Headers are any headers beyond the CORS-safe list. The CORS-safe headers are Accept, Accept-Language, Content-Language, Content-Type (for application/x-www-form-urlencoded, multipart/form-data, and text/plain only), and Range. Any other header, including Authorization, X-Requested-With, X-API-Key, and all application-specific headers, is listed explicitly.

The table below shows which headers are safe by default and which require explicit declaration.

HeaderCORS-safe by defaultRequires explicit listing
AcceptYesNo
Accept-LanguageYesNo
Content-LanguageYesNo
Content-Type (form/text values)YesNo
Content-Type (application/json)NoYes
AuthorizationNoYes
X-Requested-WithNoYes
X-API-KeyNoYes
X-Custom-HeaderNoYes

What value allows all headers? The wildcard value * in Access-Control-Allow-Headers allows all custom headers. The wildcard does not cover the Authorization header for credentialed requests. The Authorization header requires explicit listing even when the wildcard is used alongside Access-Control-Allow-Credentials.

Access-Control-Allow-Credentials and Credentialed Requests

Access-Control-Allow-Credentials tells the browser whether the server accepts cross-origin requests that include credentials. Credentials are cookies, HTTP authentication headers (Authorization), and TLS client certificates. The header accepts only the value true. Omitting the header has the same effect as setting it to false. The browser withholds credentials from cross-origin requests.

What is a credentialed request? A credentialed request is a cross-origin request sent with credentials attached. In fetch(), credentials are attached by setting credentials: ‘include’. In XMLHttpRequest, credentials are attached by setting withCredentials to true. A credentialed request sends cookies and authorization headers to the cross-origin server.

What two conditions are met for credentialed requests to succeed? Two conditions are met simultaneously. The two conditions are listed below.

  1. The server returns Access-Control-Allow-Credentials: true.
  2. The server returns a specific origin in Access-Control-Allow-Origin, not the wildcard *.

A request that receives * in Access-Control-Allow-Origin fails even when Access-Control-Allow-Credentials is true. A request that receives Access-Control-Allow-Credentials is true, but a non-matching Access-Control-Allow-Origin fails at the origin check.

Why does the wildcard fail with credentials? The wildcard fails with credentials because a wildcard CORS policy combined with credentials would allow any script on any origin to make authenticated requests using the victim’s cookies. A malicious page would read authenticated API responses using the victim’s session. The browser enforces the rule that credentials require explicit origin trust to prevent that attack.

What happens to cookies in a credentialed request? Cookies attached to a credentialed request follow the server’s cookie policies. The server sets cookies using the Set-Cookie response header. The browser stores and sends those cookies in subsequent credentialed requests to the same origin. Third-party cookie restrictions in modern browsers affect whether cookies set by a cross-origin server persist across sessions.

What does the client side need for credentialed requests? The client-side fetch() call includes credentials: ‘include’ to send cookies. Token-based authentication requires the Authorization header in the request headers alongside the credentials setting.


fetch(‘https://api.example.com/data’, {  credentials: ‘include’,  headers: {    ‘Authorization’: ‘Bearer ‘ + token,    ‘Content-Type’: ‘application/json’  }});

What is the difference between credentials: ‘include’ and credentials: ‘same-origin’? The value ‘include’ sends credentials for all requests, including cross-origin. The value ‘same-origin’ sends credentials only for same-origin requests. The value ‘omit’ never sends credentials. Cross-origin authenticated requests require ‘include’.

Access-Control-Max-Age and Preflight Caching

Access-Control-Max-Age tells the browser how long to cache the results of a preflight request, measured in seconds. The browser stores the preflight response in a cache. Subsequent requests to the same endpoint with the same method and headers use the cached response instead of sending a new OPTIONS request.

What problem does Access-Control-Max-Age solve? Preflight requests add a round-trip before each non-simple cross-origin request. A value of 0 means no caching; every request triggers a preflight. A high value (86400 seconds equals 24 hours) caches the preflight for an entire day. Applications that make many cross-origin requests to the same endpoint reduce latency by setting a high Access-Control-Max-Age value.

What is the browser maximum for Access-Control-Max-Age? Different browsers enforce different maximum values. Chrome enforces a maximum of 7200 seconds (2 hours) regardless of the server’s value. Firefox enforces a maximum of 86400 seconds (24 hours). Setting a value higher than the browser’s maximum has no additional effect. The browser uses its own cap as the effective ceiling.

What happens when the preflight cache expires? The browser sends a new OPTIONS preflight request when the cached preflight expires. The new preflight follows the same process as the first. The cache refreshes with the new response, and the max-age timer restarts.

What is the default preflight cache duration when Access-Control-Max-Age is absent? The default preflight cache duration when Access-Control-Max-Age is absent is 5 seconds in most browsers. Without the header, the browser caches the preflight for 5 seconds and re-sends the OPTIONS request after that window.

Access-Control-Expose-Headers and Browser Access Rules 

Access-Control-Expose-Headers tells the browser which response headers are accessible to JavaScript in cross-origin requests. By default, JavaScript reads only six response headers. They are Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, and Pragma. All other headers are hidden from JavaScript unless listed in Access-Control-Expose-Headers.

What headers commonly require explicit exposure? Headers that commonly require explicit exposure are listed below.

  1. Content-Disposition, for file downloads where the filename comes from the server.
  2. X-RateLimit-Limit and X-RateLimit-Remaining, for API rate limiting display.
  3. X-Request-Id, for request tracing and debugging.
  4. ETag, for conditional requests and caching.
  5. Link, for pagination links in REST APIs.

Any custom headers the application reads after a cross-origin fetch are listed in Access-Control-Expose-Headers.

What value exposes all headers? The wildcard value * in Access-Control-Expose-Headers exposes all response headers to JavaScript. The wildcard does not expose the Set-Cookie header. Set-Cookie is never exposed to JavaScript regardless of CORS configuration.

What happens if a header is not listed? JavaScript receives undefined or an empty string when reading a response header not listed in Access-Control-Expose-Headers. The header exists in the actual HTTP response. The browser blocks JavaScript access to it.

How to Configure CORS on a Server?

Configuring CORS on a server means adding the correct Access-Control response headers to every cross-origin response from that server. The configuration method differs by server runtime and platform. There are six main configuration contexts. The contexts are listed below.

  1. Enable CORS in Express (Node.js).
  2. Configure CORS in Nginx.
  3. Configure CORS in Apache.
  4. Configure CORS for Credentialed Requests.
  5. Configure Preflight Caching Correctly.
  6. When to Use a Reverse Proxy Instead of Direct CORS Configuration.

Enable CORS in Express (Node.js)

Enable CORS in Express by adding the cors middleware package or by setting response headers manually in a middleware function. The cors package is the standard approach for Express applications. Manual header setting is appropriate for fine-grained control over which endpoints receive which headers.

How to install and use the cors package? Install the cors package with npm install cors. Require the package in the application file and pass it to app.use() to enable CORS for all routes.

Basic CORS for all origins.

const express = require(‘express’);const cors = require(‘cors’);const app = express();app.use(cors());

What does app.use(cors()) without arguments do? Calling app.use(cors()) without arguments sets Access-Control-Allow-Origin: * on all responses. This configuration permits any origin to read the response. This configuration is appropriate for public APIs only.

How to restrict CORS to a specific origin? Pass an options object with the origin property set to the allowed origin string.

app.use(cors({  origin: ‘https://app.example.com’}));

How to allow multiple specific origins? Pass a function to the origin property. The function checks the incoming origin against an allowlist and calls the callback with true or an error.

const allowedOrigins = [  ‘https://app.example.com’,  ‘https://admin.example.com’];app.use(cors({  origin: function(origin, callback) {    if (!origin || allowedOrigins.indexOf(origin) !== -1) {      callback(null, true);    } else {      callback(new Error(‘Not allowed by CORS’));    }  }}));

What does that !origin check do? The !origin check permits requests with no Origin header. Requests from server-side clients (Curl, server-to-server calls) send no Origin header. Permitting requests with no origin allows those clients to access the API. Remove this check for APIs that reject non-browser requests entirely.

How to handle preflight requests in Express? The cors package handles OPTIONS preflight requests automatically when placed before route definitions. The cors middleware intercepts OPTIONS requests and returns the appropriate headers.

For manual preflight handling without the cors package.

app.use((req, res, next) => {  const origin = req.headers.origin;  const allowedOrigins = [‘https://app.example.com’, ‘https://admin.example.com’];  if (allowedOrigins.includes(origin)) {    res.header(‘Access-Control-Allow-Origin’, origin);    res.header(‘Vary’, ‘Origin’);  }  res.header(‘Access-Control-Allow-Methods’, ‘GET, POST, PUT, DELETE, OPTIONS’);  res.header(‘Access-Control-Allow-Headers’, ‘Content-Type, Authorization’);  if (req.method === ‘OPTIONS’) {    return res.sendStatus(204);  }  next();});

What does sendStatus(204) do in the preflight handler? Returning a 204 No Content status ends the OPTIONS request with an empty body. The 204 status is the standard response for successful preflight requests. The browser reads the headers and proceeds with the main request.

Configure CORS in Nginx

Configure CORS in Nginx by adding add_header directives to the location block and handling OPTIONS requests with a return 204 directive.

Add CORS headers to all responses from a location block:

location /api/ {  if ($request_method = OPTIONS) {    add_header Access-Control-Allow-Origin ‘https://app.example.com’;    add_header Access-Control-Allow-Methods ‘GET, POST, PUT, DELETE, OPTIONS’;    add_header Access-Control-Allow-Headers ‘Content-Type, Authorization’;    add_header Access-Control-Max-Age 86400;    return 204;  }  add_header Access-Control-Allow-Origin ‘https://app.example.com’;  proxy_pass http://backend;}

Why are two separate add_header blocks needed? Two separate blocks are needed because the OPTIONS preflight request returns before proxying to the backend. The first block handles the preflight and returns immediately with 204. The second block adds CORS headers to actual proxied responses.

How to support multiple origins in Nginx? Use a map directive to match the incoming Origin header against an allowlist and store the matched value in a variable.

map $http_origin $cors_origin {  default ”;  ‘https://app.example.com’ ‘https://app.example.com’;  ‘https://admin.example.com’ ‘https://admin.example.com’;}
server {  location /api/ {    add_header Access-Control-Allow-Origin $cors_origin always;    add_header Vary Origin always;    if ($request_method = OPTIONS) {      add_header Access-Control-Allow-Methods ‘GET, POST, PUT, DELETE, OPTIONS’ always;      add_header Access-Control-Allow-Headers ‘Content-Type, Authorization’ always;      add_header Access-Control-Max-Age 86400 always;      return 204;    }    proxy_pass http://backend;  }}

What does the always flag do in Nginx add_header? The always flag adds the header to all responses, including error responses (4xx, 5xx). Without always, Nginx adds the header only to successful 2xx responses. CORS headers on error responses are necessary for browser error handling to work correctly in cross-origin contexts.

What does the Vary Origin header do in Nginx? The Vary: Origin header tells caching proxies to store separate cache entries for each origin value. Without Vary: Origin, a proxy caches the response for one origin and incorrectly serves it to another origin with a different CORS header value.

Configure CORS in Apache

Configure CORS in Apache by enabling the mod_headers module and adding Header directives to the VirtualHost or Directory block.

Enable mod_headers on Debian/Ubuntu systems.

a2enmod headersservice apache2 restart

Add CORS configuration to the VirtualHost or .htaccess file.

<IfModule mod_headers.c>  Header always set Access-Control-Allow-Origin “https://app.example.com”  Header always set Access-Control-Allow-Methods “GET, POST, PUT, DELETE, OPTIONS”  Header always set Access-Control-Allow-Headers “Content-Type, Authorization”  Header always set Access-Control-Max-Age “86400”  Header always set Vary “Origin”  RewriteEngine On  RewriteCond %{REQUEST_METHOD} OPTIONS  RewriteRule ^(.*)$ $1 [R=204,L]</IfModule>

What does the RewriteRule do in the Apache configuration? The RewriteRule matches OPTIONS requests and returns a 204 status. The L flag stops further rewrite processing. The R=204 flag sends the 204 response immediately. The combination handles the preflight request without passing it to the application.

How to restrict CORS to specific origins in Apache? Use a combination of SetEnvIf and Header directives to check the incoming Origin header.

SetEnvIf Origin “^(https://app\.example\.com)$” CORS_ORIGIN=$1SetEnvIf Origin “^(https://admin\.example\.com)$” CORS_ORIGIN=$1Header always set Access-Control-Allow-Origin “%{CORS_ORIGIN}e” env=CORS_ORIGINHeader always set Vary “Origin”

What does SetEnvIf do in this configuration? SetEnvIf matches the Origin header against a regular expression. The parentheses capture the matched value. The captured value is stored in the CORS_ORIGIN environment variable. The Header directive sets the response header to the variable value only when the origin matches. Unrecognized origins produce no CORS_ORIGIN variable, so no CORS header is set.

Configure CORS for Credentialed Requests

Configure CORS for credentialed requests by setting Access-Control-Allow-Credentials to true and specifying an explicit origin in Access-Control-Allow-Origin instead of a wildcard.

In Express with the cors package.

app.use(cors({  origin: ‘https://app.example.com’,  credentials: true}));

In the manual Express middleware.

app.use((req, res, next) => {  const origin = req.headers.origin;  const allowedOrigins = [‘https://app.example.com’];  if (allowedOrigins.includes(origin)) {    res.header(‘Access-Control-Allow-Origin’, origin);    res.header(‘Access-Control-Allow-Credentials’, ‘true’);    res.header(‘Vary’, ‘Origin’);  }  res.header(‘Access-Control-Allow-Methods’, ‘GET, POST, PUT, DELETE, OPTIONS’);  res.header(‘Access-Control-Allow-Headers’, ‘Content-Type, Authorization’);  if (req.method === ‘OPTIONS’) {    return res.sendStatus(204);  }  next();});

What breaks if a wildcard is used with credentials? The browser blocks the request. The browser console shows an error stating that the wildcard is not allowed when credentials are included. The application receives no response data. This is a browser-enforced restriction that cannot be worked around at the client level.

What cookie attributes affect credentialed cross-origin requests? Three cookie attributes determine whether cookies send in credentialed cross-origin requests. The three attributes are listed below.

  1. SameSite: None is required for cookies to send in cross-site contexts. SameSite: Strict and SameSite: Lax prevent cookies from sending in cross-origin requests even with correct CORS configuration.
  2. Secure: true is required when SameSite is set to None. A cookie with SameSite: None but without Secure is rejected by modern browsers.
  3. Domain: the cookie’s domain attribute covers the API’s domain. A cookie scoped to .example.com sends to api.example.com and app.example.com.

Configure Preflight Caching Correctly

Configure preflight caching by setting the Access-Control-Max-Age header in the OPTIONS response to a value that balances caching efficiency against the frequency of CORS policy changes.

What value to set for Access-Control-Max-Age? Set Access-Control-Max-Age to 7200 (2 hours) for production APIs where the CORS policy does not change frequently. This value matches Chrome’s maximum effective cache duration. Set it to 600 (10 minutes) for APIs where allowed origins or methods change regularly. Set it to 0 during debugging to disable preflight caching and observe every preflight request in DevTools.

What happens when a CORS policy changes? Cached preflight responses continue to be used until they expire. A browser that cached a preflight with a 2-hour max-age continues to skip the OPTIONS request for 2 hours even after the server’s CORS policy changes. During a CORS policy update, temporarily reduce Access-Control-Max-Age to a low value (60 seconds), wait for caches to expire, then deploy the policy change, then restore the high value.

How does preflight caching interact with browser limits? Chrome caps the effective max-age at 7200 seconds. Firefox caps it at 86400 seconds. Setting a value above the browser’s cap has no effect. The effective cache duration is the minimum of the server’s value and the browser’s cap.

Use a Reverse Proxy Instead of Direct CORS Configuration

Use a reverse proxy instead of direct CORS configuration when the target server cannot be modified, when centralized CORS policy management across multiple backends is required, or when CORS headers are added to third-party services that the application does not control.

What problem does a reverse proxy solve? A reverse proxy routes requests from the browser to the backend server and adds CORS headers at the proxy layer. The browser sends the request to the proxy’s origin. The proxy forwards the request to the backend. The browser sees a same-origin request to the proxy, eliminating CORS for the browser. CORS headers on the proxy’s responses control what the proxy exposes to other origins.

When is a reverse proxy the correct solution? A reverse proxy is the correct solution in four cases. The four cases are listed below.

  1. The target API is a third-party service that does not return CORS headers and cannot be configured.
  2. The application requires CORS policy management in one place across multiple backend services.
  3. The backend server is a legacy system that cannot be modified to return response headers.
  4. The CORS policy is complex enough that maintaining it across multiple services introduces error risk.

What is the tradeoff? A reverse proxy adds a network hop and requires its own deployment, configuration, and maintenance. Direct CORS configuration on the backend requires no additional infrastructure. Direct configuration is preferred when the application controls its own backends. A reverse proxy is preferred when the backend cannot be changed or when multiple backends need centralized policy control.

How to Read and Fix a CORS Error?

CORS errors appear in the browser console as blocked request messages that identify the specific header or policy that caused the block. Reading the error message identifies the cause. The cause determines the fix. The fix is applied on the server, not the client.

Why is the fix always on the server? The fix is always on the server because CORS is controlled by server-returned headers. The browser enforces CORS; the server configures it. A client-side workaround does not fix the underlying CORS configuration. The correct fix adds the required headers to the server response.

What does the browser console show for a CORS error? The browser console shows a red error message beginning with “Access to fetch at [URL] from origin [origin] has been blocked by CORS policy.” The message continues with the specific reason. Common reasons are a missing header, a mismatched origin value, or a blocked method.

How to Diagnose Missing or Incorrect CORS Headers?

Diagnose CORS errors by opening the browser’s DevTools Network tab, finding the blocked request, and inspecting the response headers to identify which CORS header is absent or incorrect.

What does the Network tab show? The Network tab shows the full list of request and response headers for every request. A blocked CORS request shows the request headers (including Origin) and the response headers. The absence of Access-Control-Allow-Origin in the response headers confirms that the server is not returning CORS headers for that endpoint.

What are the three most common CORS error messages? There are three common CORS error messages. The three messages are listed below.

  1. “No ‘Access-Control-Allow-Origin’ header is present on the requested resource.” The server returned no Access-Control-Allow-Origin header. The server has no CORS configuration, or the CORS configuration does not apply to this endpoint.
  2. “The ‘Access-Control-Allow-Origin’ header has a value that is not equal to the supplied origin.” The server returned an Access-Control-Allow-Origin header, but the value does not match the requesting origin. The origin is not on the server’s allowlist.
  3. “Method PUT is not allowed by Access-Control-Allow-Methods in preflight response.” The server returned a preflight response that does not list the requested HTTP method. The server’s CORS configuration does not permit the method from this origin.

How to confirm an origin mismatch? Open the Network tab. Find the blocked request. Check the Origin header in the request. Check the Access-Control-Allow-Origin header in the response. Compare the two values. A mismatch confirms the server’s CORS allowlist does not include the requesting origin.

How to confirm a preflight failure? Open the Network tab. Find the OPTIONS request that precedes the blocked main request. Check the response to the OPTIONS request. A 401, 403, or 404 response means the server is blocking the preflight with authentication or routing logic. A 200 or 204 response with missing CORS headers means the CORS middleware is not applied to the OPTIONS route.

What does Curl show when testing CORS headers? Curl with the -v flag shows the full request and response headers. Send a preflight simulation with the Origin and Access-Control-Request-Method headers to confirm the server returns the correct response.

curl -H “Origin: https://app.example.com” \  -H “Access-Control-Request-Method: PUT” \  -H “Access-Control-Request-Headers: Content-Type” \  -X OPTIONS \  -v \  https://api.example.com/data

The response includes Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers with the correct values.

Step-by-Step Workflow for Fixing CORS Errors

Fix CORS errors by following a four-step workflow that moves from diagnosis to server-side correction and verification.

The four steps are listed below.

1. Identify the error type. Read the browser console error message. Identify whether the issue is a missing header, a mismatched origin, a blocked method, or a blocked header. The error message names the specific failure.

2. Check the server’s CORS configuration. Verify that the middleware or header configuration applies to the endpoint returning the error. Verify that the requesting origin appears in the allowlist. Verify that the requested method and custom headers are listed.

3. Test the fix with Curl. Send a direct request with the Origin header to confirm the server returns the correct headers. For preflight-dependent requests, simulate the OPTIONS request as shown above. Confirm the response includes all required CORS headers.

4. Test in the browser. Reload the page and repeat the request. Confirm the error is gone in the console. Confirm the response data reaches the application code.

What if the server returns headers, but the error persists? Check whether the request is credentialed and the response returns a wildcard origin. A credentialed request with * in Access-Control-Allow-Origin always fails. Check whether the requesting origin has a trailing slash. The values https://app.example.com/ and https://app.example.com are different, and the comparison is exact. Check whether the request method is listed exactly (PUT, not put) in Access-Control-Allow-Methods.

What does a successful CORS exchange look like in the Network tab? A successful exchange shows an OPTIONS request with a 204 response, followed immediately by the main request with a 200 response. The OPTIONS response includes Access-Control-Allow-Origin matching the page origin. The main response includes Access-Control-Allow-Origin. The application code receives the response data without error.

What Are the Best Practices for Configuring CORS?

Best practices for configuring CORS reduce the risk of misconfiguration, prevent security vulnerabilities, and maintain correct browser behavior across all request types. There are six main best practices. The practices are listed below.

  1. Use Explicit Origins Instead of Wildcards for Authenticated APIs.
  2. Limit Allowed Methods and Headers.
  3. Configure Credentialed Requests Carefully.
  4. Cache Preflight Requests With Access-Control-Max-Age.
  5. Validate Cross-Origin Configuration in Browser DevTools.
  6. Separate CORS Configuration From Authentication Logic.

Use Explicit Origins Instead of Wildcards for Authenticated APIs

Use explicit origins instead of wildcards for any API endpoint that requires authentication, processes user data, or returns sensitive information. The wildcard (*) in Access-Control-Allow-Origin permits any origin to read the response. For public, unauthenticated API endpoints returning non-sensitive data, the wildcard is acceptable. For endpoints that require authentication or return user-specific data, the wildcard creates an unnecessary exposure surface.

What risk does a wildcard create for authenticated APIs? A wildcard on an authenticated endpoint permits any origin on the web to trigger requests to the endpoint from a user’s browser. The browser sends the request. The response is readable by the script on that unauthorized origin. A wildcard does not permit credential forwarding, but it permits reading responses from endpoints that return exploitable information without authentication.

How to implement an explicit origin allowlist? Maintain a list of permitted origins in the server’s configuration or environment variables. Check each incoming Origin header against the list. Return the matching origin in Access-Control-Allow-Origin. Return Vary: Origin to prevent caching conflicts.

const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS.split(‘,’);function getCorsOrigin(requestOrigin) {  return allowedOrigins.includes(requestOrigin) ? requestOrigin : null;}

What origins to include in the allowlist? Include all production origins that legitimately access the API. Include staging and development origins in separate environment-specific configurations. Do not include localhost in production allowlists. Do not include origins from third-party domains that the application does not control.

How to manage allow lists across environments? Store allowed origins in environment variables. Use a separate value for each environment (development, staging, production). The production allowlist contains only the production front-end origins. The development allowlist includes localhost 3000 or the local development origin. Environment variable management prevents development origins from appearing in production configurations.

Limit Allowed Methods and Headers

Limit Access-Control-Allow-Methods to the specific HTTP methods the endpoint actually accepts. Listing methods the endpoint does not implement creates no security benefit and misleads clients about what is available.

What methods to list? List only the methods the endpoint handles. A read-only endpoint lists GET and HEAD. A standard REST API resource lists GET, POST, PUT, DELETE, PATCH, and OPTIONS. The OPTIONS method is always listed to allow preflight requests to succeed from all affected origins.

Why does listing extra methods matter? Listing extra methods expands the surface area for unintended requests. An endpoint that lists DELETE but does not implement deletion returns a 405 Method Not Allowed from the application. The browser still attempts the request because the CORS policy permitted it. Tight method lists reduce unintended request attempts.

How to limit allowed headers? List only the headers the application reads from cross-origin requests. Authorization and Content-Type cover most API use cases. Custom application headers (X-API-Key, X-Tenant-Id) are added as needed. The wildcard * for headers allows all custom headers, which is broader than most APIs require.

What is the risk of the * wildcard for headers? The wildcard for Access-Control-Allow-Headers allows browsers to send any header in cross-origin requests. An unexpected header from a malicious page passes through the CORS check. Explicit header lists narrow the accepted surface area. The wildcard remains appropriate for internal APIs where the set of headers is unpredictable, but the origin is controlled.

Configure Credentialed Requests Carefully

Configure credentialed requests by requiring both Access-Control-Allow-Credentials: true and an explicit origin in Access-Control-Allow-Origin on every endpoint that accepts authenticated cross-origin requests.

What additional checks matter for credentialed endpoints? Three additional checks matter for credentialed endpoints. The three checks are listed below.

  1. Confirm the cookie’s SameSite attribute is set to None with Secure: true. A cookie with SameSite: Strict does not send in cross-origin requests even with CORS configured correctly. SameSite: None requires the Secure attribute. The combination permits cookies in cross-site contexts over HTTPS.
  2. Confirm the cookie’s Domain attribute covers the API’s domain. A cookie scoped to .example.com sends to api.example.com and app.example.com. A cookie scoped to api.example.com sends only to api.example.com.
  3. Confirm HTTPS is active on both origins. Cookies with Secure: true send only over HTTPS. A credentialed CORS request over HTTP with a Secure cookie sends no credentials, causing authentication failures.

What does SameSite: Strict on an authentication cookie produce? The browser does not send the cookie in cross-origin requests even when CORS is correctly configured. The API receives the request without the authentication cookie. The API returns a 401 Unauthorized. The fix changes the cookie’s SameSite attribute to None and adds Secure.

How to test credentialed request configuration? Open DevTools. Find the cross-origin request in the Network tab. Check the request headers for the Cookie header. A missing Cookie header in a credentialed request indicates SameSite restriction is blocking it. Check the response headers for Access-Control-Allow-Credentials. Check that Access-Control-Allow-Origin contains the exact requesting origin.

Cache Preflight Requests With Access-Control-Max-Age

Set Access-Control-Max-Age to a value high enough to reduce preflight overhead without making CORS policy updates difficult to deploy.

What value to use in production? Use 7200 seconds (2 hours) as the default for production APIs. This value matches Chrome’s maximum effective cache duration. The value eliminates repeated preflight requests for most user sessions. A CORS policy change takes at most 2 hours to propagate to all active browsers after deployment.

What value to use in development? Use 0 in development environments. A value of 0 disables preflight caching. Every request triggers a new OPTIONS preflight. This makes CORS behavior immediately observable and allows policy changes to take effect instantly during testing.

How to deploy a CORS policy change with minimal disruption? Reduce Access-Control-Max-Age to 60 seconds before deploying the CORS policy change. Wait for all existing caches to expire. Deploy the CORS policy change. Restore Access-Control-Max-Age to the production value after confirming the change is working correctly.

How does Access-Control-Max-Age interact with CDN caching? A CDN in front of the API caches OPTIONS responses if the CDN treats 204 responses as cacheable. CDN-cached preflight responses with an expired max-age continue to be served from the CDN cache. Configure the CDN to respect the Cache-Control header on OPTIONS responses, or exclude OPTIONS from CDN caching entirely.

Validate Cross-Origin Configuration in Browser DevTools

Validate CORS configuration by using the browser’s Network tab to inspect request and response headers, confirm the correct headers are present, and verify that the application receives response data.

What to check in the Network tab for a preflighted request? Open the Network tab before making the cross-origin request. Find the OPTIONS preflight request. Confirm it returns a 200 or 204 status. Check the response headers for Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers. Find the main request that follows. Confirm it returns the expected status code. Confirm Access-Control-Allow-Origin is present in the main response.

How to filter the Network tab for CORS-related requests? Use the “All” filter in the Network tab to see OPTIONS requests alongside main requests. Some DevTools filter out OPTIONS requests by default. The Fetch/XHR filter shows fetch() and XMLHttpRequest calls. The “other” filter shows OPTIONS preflight requests in Chrome DevTools.

How to test for credentialed request configuration? Open the Application tab in DevTools. Check the Cookies panel for the target domain. Confirm the expected cookies are present. Verify the SameSite and Secure attributes. Repeat the credentialed request. Check the request headers in the Network tab. Confirm the Cookie header is present. Confirm the response data is accessible.

What does a successful CORS exchange look like? A successful exchange shows an OPTIONS request with a 204 response immediately followed by the main request with a 200 response. The OPTIONS response includes Access-Control-Allow-Origin exactly matching the page’s origin. The main response includes Access-Control-Allow-Origin. No red CORS errors appear in the console.

Separate CORS Configuration From Authentication Logic

Separate CORS configuration from authentication logic by placing CORS headers in middleware that runs before authentication checks and by never using CORS headers as a substitute for authentication.

Why does CORS middleware run before authentication? CORS preflight requests (OPTIONS) are sent without credentials. An authentication middleware that rejects unauthenticated requests rejects the preflight before the CORS headers are returned. The browser receives a 401 or 403 error instead of the expected CORS headers. The main request is never sent. The browser logs a CORS error, but the actual cause is authentication blocking the preflight.

What is the correct middleware order in Express? Place cors() before any authentication middleware.

app.use(cors({ origin: ‘https://app.example.com’, credentials: true }));app.use(authMiddleware);app.use(‘/api’, apiRoutes);

What is the wrong pattern? The wrong pattern places authentication before CORS. An OPTIONS request from the browser hits the authentication middleware first. The middleware returns a 401. The CORS headers are never added. The browser logs a CORS error. The actual error is an authentication failure blocking the preflight.

Why does CORS not replace authentication? CORS controls which browser origins read responses. CORS does not verify who is making the request. A server with CORS configuration but no authentication accepts requests from allowed browser origins without verifying identity. Authentication verifies identity. Authorization verifies permissions. CORS verifies origin. All three are independent requirements for a secure API.

What happens when rate limiting applies to OPTIONS requests? A rate limiter that counts OPTIONS requests against the same limit as main requests triggers a 429 Too Many Requests response on the preflight. The browser sees a 429 without CORS headers and logs a CORS error. The fix excludes OPTIONS requests from rate limiting or applies a separate, higher limit to OPTIONS requests.

What Are the Most Common CORS Misconfigurations?

CORS misconfigurations are server-side configuration errors that either block legitimate cross-origin requests or create security vulnerabilities by granting access to unauthorized origins. There are five main categories of CORS misconfiguration. The categories are listed below.

  1. Wildcard Origins With Credentialed Requests.
  2. Overly Permissive Origin Whitelists.
  3. Misconfigured Preflight Responses.
  4. Incorrect Handling of Null Origins.
  5. Mixing Authentication Logic With CORS Rules.

Wildcard Origins With Credentialed Requests 

Wildcard origins with credentialed requests are the most common CORS misconfiguration because developers set Access-Control-Allow-Origin to resolve CORS errors quickly and then separately add Access-Control-Allow-Credentials to enable cookie-based authentication. The browser blocks the combination. The server returns both headers. The browser enforces the rule that the wildcard origin and credentials cannot coexist. The application fails when authentication is enabled.

What is the correct configuration? Replace the wildcard with the explicit origin. Return Access-Control-Allow-Origin: https://app.example.com alongside Access-Control-Allow-Credentials: true. The browser accepts the combination and forwards credentials to the server.

What is the reflected-origin security risk? A server that dynamically reflects the incoming Origin header in Access-Control-Allow-Origin without validating it against an allowlist, combined with Access-Control-Allow-Credentials, allows any origin to make authenticated requests. Any page on the web triggers requests to the API using the victim’s cookies. This is a critical vulnerability that exposes authenticated user data to arbitrary origins.

How to detect reflected-origin misconfiguration? Send a request with a fabricated Origin header (Origin: https://attacker.example.com) and observe whether the response reflects that origin in Access-Control-Allow-Origin. A safe server returns the declared origin from its allowlist or returns no header. A misconfigured server returns Access-Control-Allow-Origin: https://attacker.example.com.

What is the correct pattern for dynamic origin reflection? Validate the incoming Origin header against a strict allowlist before reflecting it. Never reflect an origin not on the allowlist. Return an error or no CORS header for unrecognized origins.

const allowedOrigins = [‘https://app.example.com’];app.use((req, res, next) => {  const origin = req.headers.origin;  if (allowedOrigins.includes(origin)) {    res.header(‘Access-Control-Allow-Origin’, origin);    res.header(‘Access-Control-Allow-Credentials’, ‘true’);    res.header(‘Vary’, ‘Origin’);  }  next();});

Overly Permissive Origin Whitelists 

Overly permissive origin whitelists occur when the allowlist validation logic uses regex patterns that match more origins than intended, allowing unauthorized subdomains or related domains to pass the check.

What is the most common regex mistake? A regex pattern intended to match https://example.com written without anchors (https://example\.com) matches https://attackerexample.com and https://evil-example.com. The dot in the domain matches any character without escaping. The string is found as a substring of the longer domain. The correct pattern uses start and end anchors: ^https://example\.com$.

What is a subdomain wildcard misconfiguration? A check written to allow any subdomain of example.com using the pattern .*\.example\.com permits attacker.example.com. Subdomain wildcards are safe only when all subdomains under the domain are controlled and secured. An attacker who gains control of any subdomain (through an XSS vulnerability, a compromised third-party script, or an abandoned subdomain) exploits the wildcard to make authenticated requests.

What is the risk of abandoned subdomains? An abandoned subdomain that previously hosted a service is claimed by an attacker through DNS takeover. A CORS allowlist that permits *.example.com grants the attacker’s claimed subdomain full access to authenticated API responses. Subdomain wildcards require an active inventory and decommissioning process for all subdomains.

What is the correct check for multiple allowed origins? Use exact string comparison against a list of known origins. Do not use substring matching, suffix matching, or loose regex. The comparison is exact. The incoming origin is equal to one of the listed values character-for-character.

Misconfigured Preflight Responses

Misconfigured preflight responses occur when the server handles OPTIONS requests differently from other requests, causing CORS headers to be absent from the preflight response even though they are present on the main response.

What causes this misconfiguration? Authentication middleware that runs before CORS middleware intercepts OPTIONS requests and returns a 401 before the CORS headers are added. Rate-limiting middleware counts OPTIONS requests against the rate limit and returns a 429 on the preflight. Route-specific CORS configuration that applies only to POST or GET routes omits coverage of OPTIONS.

What does the browser see? The browser sends an OPTIONS request and receives a 401, 429, or 403. The browser treats the preflight as failed. The browser logs a CORS error. The main request is never sent. The developer sees a CORS error, but the root cause is authentication or rate limiting blocking the preflight, not a missing CORS header.

How to fix misconfigured preflight responses? Apply CORS middleware before authentication middleware. Exclude OPTIONS requests from rate limiting. Apply CORS configuration to all request methods, including OPTIONS. Test the preflight directly with Curl to confirm the 204 response and correct headers before debugging the application logic.

How to verify OPTIONS request handling in Express? Add a direct OPTIONS route handler to confirm the response before the main CORS middleware is in place.

app.options(‘*’, cors());app.use(cors({ origin: ‘https://app.example.com’ }));

The app.options(‘*’, cors()) line handles all preflight requests explicitly before any other middleware.

Incorrect Handling of Null Origins

Incorrect handling of null origins occurs when server-side CORS logic adds the string “null” to the allowlist, permitting requests from sandboxed contexts that send Origin: null.

When does a browser send Origin: null? A browser sends Origin: null in four situations. The four situations are listed below.

  1. Requests from sandboxed iframes with the sandbox attribute and no allow-same-origin permission.
  2. Requests from data: URIs.
  3. Requests from file:// URLs.
  4. Requests that were redirected in a way that strips the origin.

What is the attack scenario for null origins? An attacker embeds a sandboxed iframe on a malicious page. The iframe contains a data URI with JavaScript code. The JavaScript makes a cross-origin request to the target API. The browser sends Origin: null. A server that permits null origin returns Access-Control-Allow-Origin: null in the response. The sandboxed JavaScript reads the response data.

What is the correct handling for null origins? Never add null to the origin allowlist. Return no CORS headers for requests with Origin: null. Null origins indicate requests from contexts that legitimate web applications do not generate in production.

How does a server accidentally allow null origins? A server that reflects the incoming Origin header without validation reflects the literal string “null” when the browser sends Origin: null. The reflection produces Access-Control-Allow-Origin: null. Combined with Access-Control-Allow-Credentials: true, this creates an authenticated null-origin vulnerability. The fix is an origin allowlist validation that rejects any value not matching an expected format.

Mixing Authentication Logic With CORS Rules

Mixing authentication logic with CORS rules occurs when CORS headers are added conditionally based on whether the request is authenticated, causing preflight requests to fail because they carry no credentials.

What is the pattern of this error? A middleware function checks for a valid session cookie or authorization header before adding CORS headers. OPTIONS preflight requests carry no credentials. The middleware sees no credentials, skips the CORS headers, and returns a plain 200 or 401 response. The browser receives no CORS headers from the preflight and blocks the main request.

What does the developer see? The developer sees a CORS error in the browser console. The Network tab shows the OPTIONS request returning without CORS headers. The developer investigates the CORS configuration and finds that it exists. The developer does not immediately see that authentication is blocking the preflight, because the error message says “CORS policy”, not “authentication.”

What does the correct pattern look like? CORS headers are added unconditionally for all requests from allowed origins, regardless of authentication status. Authentication is enforced separately for non-OPTIONS requests.

app.use((req, res, next) => {  const origin = req.headers.origin;  if (allowedOrigins.includes(origin)) {    res.header(‘Access-Control-Allow-Origin’, origin);    res.header(‘Access-Control-Allow-Credentials’, ‘true’);    res.header(‘Vary’, ‘Origin’);  }  if (req.method === ‘OPTIONS’) {    res.header(‘Access-Control-Allow-Methods’, ‘GET, POST, PUT, DELETE’);    res.header(‘Access-Control-Allow-Headers’, ‘Content-Type, Authorization’);    return res.sendStatus(204);  }  authMiddleware(req, res, next);});

What is the risk of the mixed pattern beyond the functional failure? The mixed pattern creates inconsistent behavior that is difficult to debug. Requests succeed or fail based on the authentication state rather than the CORS policy. Testing CORS without authentication returns different results from testing with authentication. The separation of concerns between CORS and authentication eliminates both the functional failure and the debugging complexity.

What Security Risks Are Associated With CORS?

CORS security risks arise from misconfiguration, not from the CORS mechanism itself. A correctly configured CORS policy restricts cross-origin access to authorized origins. A misconfigured CORS policy creates vulnerabilities that allow cross-origin attacks against authenticated users.

What is the primary CORS security risk? The primary security risk is reflected in origin misconfiguration combined with credentialed requests. A server that reflects any incoming Origin value in Access-Control-Allow-Origin without allowlist validation, combined with Access-Control-Allow-Credentials, allows any origin to make authenticated requests using the victim’s cookies. An attacker who places a malicious script on any page triggers requests to the vulnerable API using the victim’s session.

What is the whitelist parsing risk? Whitelist parsing errors allow unintended origins to pass the validation check. A regex pattern that does not anchor the start and end of the origin string permits origins that contain the expected string as a substring. A check for example.com passes evil-example.com. A check for .example.com passes attackerexample.com. Exact string comparison eliminates this class of risk.

What is the null origin risk? A server that explicitly permits the null origin in its allowlist allows sandboxed iframes and data, URIs to make requests. Attackers construct sandboxed iframes with data, URI JavaScript, and host them on any page to exploit this configuration.

What is the XSS-in-trusted-domain risk? A CORS allowlist that permits a subdomain of a trusted domain is vulnerable when that subdomain has an XSS vulnerability. An attacker exploits the XSS to run a script on the trusted subdomain. The script makes authenticated cross-origin requests to the target API. The CORS policy permits the request because the origin is a trusted subdomain. An XSS vulnerability on any permitted subdomain becomes an amplifier for CORS-based data exfiltration.

What is the HTTP/HTTPS mixing risk? A secure HTTPS API that lists an HTTP version of the same domain in its CORS allowlist accepts requests from the insecure origin. An HTTP page is vulnerable to traffic interception. An attacker on the same network intercepts the page and injects malicious scripts. Those scripts make CORS-permitted requests to the HTTPS API using the victim’s cookies. The fix removes all HTTP origins from the allowlist of any HTTPS API.

What is the intranet exposure risk? An internal API that returns Access-Control-Allow-Origin: * allows external websites to probe the intranet from within a user’s browser. A user visiting a malicious page on the public internet triggers requests from the browser to internal IP addresses. Internal servers that permit the wildcard return responses to the malicious script. The malicious page reads internal data from services that the user’s browser accesses on the local network.

What is the relationship between CORS and CSRF? CORS is not a protection against CSRF (Cross-Site Request Forgery). CORS governs which origins read responses. CSRF attacks forge requests that change state without relying on reading the response. A CSRF attack uses a form or image tag to trigger a state-changing request. CORS does not block form submissions or image loads. CSRF protection requires CSRF tokens or SameSite cookie attributes, not CORS headers.

What Are the Limitations of CORS?

CORS has four main limitations that define what it does and cannot do in the context of web security and cross-origin communication. The four limitations are listed below.

  1. CORS applies only to browser-initiated scripts (fetch() and XMLHttpRequest).
  2. CORS does not protect server-side resources from direct access outside a browser.
  3. CORS does not provide authentication or authorization.
  4. CORS cannot prevent all forms of cross-origin information leakage.

What is the scope limitation of CORS? CORS applies only to requests initiated by browser scripts. Cross-origin loads triggered by HTML elements (img, script, link, form) are not subject to CORS. A cross-origin image loads regardless of CORS headers. A cross-origin script loads regardless of CORS headers. CORS governs only programmatic access to cross-origin response bodies, not passive resource loading.

What is the server-side limitation? A server cannot use CORS to block requests from non-browser clients. Curl, Postman, server-side HTTP clients, and automated scripts all bypass CORS because they are not browsers. CORS does not protect APIs from unauthorized programmatic access. API authentication and authorization protect server resources. CORS restricts browser-based scripts from reading responses without explicit permission.

What is the authentication limitation? CORS does not authenticate requesters. A correctly configured CORS policy permits requests from the authorized origin. That permission does not mean the request comes from an authorized user. Authentication (API keys, JWTs, session cookies) remains required for resource protection. CORS and authentication solve different problems and operate independently.

What is the data leakage limitation? CORS governs response body access for scripts. Side-channel information still leaks through CORS in some cases. The browser reveals whether a request succeeded even for non-CORS responses. Timing information from response delays leaks through CORS. The fact that a resource exists at a URL leaks through cross-origin image loads. CORS is not a complete defense against all forms of information disclosure.

What does CORS not protect against? CORS does not protect against CSRF, clickjacking, cross-site script inclusion (XSSI), or timing attacks. Each of those requires a separate defense. CSRF requires SameSite cookies or CSRF tokens. Clickjacking requires X-Frame-Options or Content-Security-Policy frame-ancestors. XSSI requires JSON security prefixes or proper Content-Type enforcement.

Is CORS a Security Feature or a Restriction?

CORS is both a security feature and a restriction. CORS restricts browser scripts from reading cross-origin responses by default. That restriction is a security feature: it prevents scripts on a malicious page from reading responses from a different origin using the victim’s credentials.

The security feature operates from the browser. The browser enforces the restriction. The server configures exceptions to the restriction through CORS headers. A server that returns Access-Control-Allow-Origin loosens the restriction for authorized origins. A server that returns no CORS headers maintains the restriction for all cross-origin requests.

The characterization depends on the perspective. A developer who cannot load data from a cross-origin API experiences CORS as a restriction. A security team that relies on CORS to prevent cross-origin data theft experiences it as a security feature. Both characterizations are accurate.

What is the net security effect of correct CORS configuration? The net security effect is positive. CORS narrows the set of origins that read sensitive API responses. An API that returns Access-Control-Allow-Origin: * provides no origin restriction. An API that returns Access-Control-Allow-Origin: https://app.example.com provides meaningful origin-level access control on top of authentication.

What Does “No Access-Control-Allow-Origin Header Is Present” Mean?

“No ‘Access-Control-Allow-Origin’ header is present on the requested resource” means the server returned a response without an Access-Control-Allow-Origin header, and the browser blocked the script from accessing the response. This is the most common CORS error in browser consoles.

What causes this error? There are three common causes. The three causes are listed below.

  1. The server has no CORS configuration. The response contains no CORS headers for any origin. The fix is to add CORS middleware or header configuration to the server for the relevant endpoints.
  2. The server has CORS configuration, but the requesting origin is not on the allowlist. The server returns no CORS headers for unrecognized origins. The fix is to add the requesting origin to the server’s allowlist.
  3. The server has CORS configuration, but it applies only to specific routes or environments. The requested endpoint falls outside the CORS middleware’s scope. The fix is to extend the CORS middleware to cover the failing endpoint.

What does the error not mean? The error does not mean the request failed on the server. The server received the request, processed it, and returned a response. The browser received that response. The browser blocked script access to the response because of the missing header. The server-side operation completed normally.

How to fix this error? Add the Access-Control-Allow-Origin header to the server’s response for the failing endpoint. Set the value to the requesting origin or to the wildcard. Test with Curl to confirm the header is returned. Reload the page and confirm the browser console error is gone.

Picture of Manick Bhan

Agentic SEO and AI Visibility Start Here

Loading Star Icon Ask Atlas Agent what to improve. We'll start with your website.
Loading Star Icon

Join Our Community Of SEO Experts Today!

Related Reads to Boost Your SEO Knowledge

Visualize Your SEO Success: Expert Videos & Strategies

Real Success Stories: In-Depth Case Studies

Ready to Replace Your SEO Stack With a Smarter System?

If Any of These Sound Familiar, It’s Time for an Enterprise SEO Solution:

25 - 1000+ websites being managed
25 - 1000+ PPC accounts being managed
25 - 1000+ GBP accounts being managed