CORS Configuration: Cross-Origin Resource Sharing Guide

Type: Software Reference Confidence: 0.94 Sources: 7 Verified: 2026-02-27 Freshness: 2026-02-27

TL;DR

Constraints

Quick Reference

HeaderPurposeValuesSecurity Risk if Wrong
Access-Control-Allow-OriginWhich origin(s) may read the responseExact origin, * (public), or omitReflecting any origin = universal CORS bypass
Access-Control-Allow-MethodsAllowed HTTP methods for actual requestGET, POST, PUT, DELETEOverly permissive methods expand attack surface
Access-Control-Allow-HeadersCustom headers the client may sendContent-Type, AuthorizationAllowing * with credentials is rejected
Access-Control-Allow-CredentialsPermits cookies/auth in cross-origin requeststrue or omitMust pair with explicit origin, never *
Access-Control-Expose-HeadersResponse headers client JS can readX-Request-Id, X-Total-CountOmitting hides custom headers from fetch()
Access-Control-Max-AgeSeconds to cache preflight results600 to 86400Too low = excessive preflights; too high = stale policy
Vary: OriginTells caches response differs by OriginAlways include when ACAO is not *Omitting causes caches to serve wrong CORS headers
Access-Control-Request-Method(Request) Method browser plans to useSent automatically in preflightN/A (read-only)
Access-Control-Request-Headers(Request) Custom headers browser will sendSent automatically in preflightN/A (read-only)

Decision Tree

START: Does your API need cross-origin access?
├── NO → Do not set any CORS headers (same-origin policy protects you)
├── YES ↓
│   ├── Does the request carry cookies or Authorization headers?
│   │   ├── YES (credentialed) → Set ACAO to exact origin from allowlist + Credentials: true + Vary: Origin
│   │   └── NO (public) ↓
│   │       ├── Fully public API? → Access-Control-Allow-Origin: *
│   │       └── Restricted public → Set specific origin(s) from allowlist
│   ├── Uses non-simple methods (PUT, DELETE, PATCH)?
│   │   ├── YES → Handle OPTIONS preflight: return 200/204 with Allow-Methods, Allow-Headers, Max-Age
│   │   └── NO → No preflight needed, set ACAO on response
│   └── Multiple frontends need access?
│       ├── YES → Validate Origin against allowlist at runtime + Vary: Origin
│       └── NO → Hardcode the single allowed origin

Step-by-Step Guide

1. Identify your cross-origin requirements

Determine which origins need access, whether requests carry credentials, and which HTTP methods and custom headers are needed. [src2]

Questions to answer:
- Frontend origin(s): https://app.example.com, https://staging.example.com
- Credentials needed? Yes (session cookies) / No (public API)
- Methods needed: GET, POST, PUT, DELETE
- Custom headers: Authorization, Content-Type, X-Request-Id

Verify: Check browser DevTools > Network tab for blocked CORS requests (they appear as errors in the Console).

2. Implement origin validation on the server

Never reflect the Origin header blindly. Validate against an allowlist and set appropriate response headers. [src3]

const ALLOWED_ORIGINS = new Set([
  'https://app.example.com',
  'https://staging.example.com'
]);

function getCorsOrigin(requestOrigin) {
  if (ALLOWED_ORIGINS.has(requestOrigin)) {
    return requestOrigin;
  }
  return null;
}

Verify: curl -H "Origin: https://evil.com" -I https://your-api.com/data -- should NOT return Access-Control-Allow-Origin: https://evil.com

3. Handle preflight (OPTIONS) requests

Browsers send a preflight OPTIONS request before any non-simple cross-origin request. Your server must respond with correct CORS headers. [src1]

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600
Vary: Origin

Verify: curl -X OPTIONS -H "Origin: https://app.example.com" -H "Access-Control-Request-Method: PUT" -I https://your-api.com/data

4. Set headers on actual responses

Every cross-origin response (not just preflight) must include the Access-Control-Allow-Origin header. [src1]

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-Request-Id
Vary: Origin
Content-Type: application/json

Verify: In browser DevTools > Network, confirm response includes Access-Control-Allow-Origin matching the requesting origin.

5. Test with real browsers and curl

Automated tests should verify CORS headers for allowed origins, blocked origins, and preflight behavior. [src7]

# Test allowed origin
curl -sI -H "Origin: https://app.example.com" https://your-api.com/data \
  | grep -i access-control

# Test blocked origin
curl -sI -H "Origin: https://evil.com" https://your-api.com/data \
  | grep -i access-control

# Test preflight
curl -sI -X OPTIONS \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: DELETE" \
  https://your-api.com/data | grep -i access-control

Verify: Allowed origin returns correct ACAO; blocked origin returns no ACAO; preflight returns Allow-Methods.

Code Examples

Express.js (Node.js): Secure CORS with allowlist

const express = require('express');  // ^4.18.0
const cors = require('cors');        // ^2.8.5

const ALLOWED_ORIGINS = [
  'https://app.example.com',
  'https://staging.example.com'
];

const app = express();
app.use(cors({
  origin: (origin, callback) => {
    if (!origin) return callback(null, true);
    if (ALLOWED_ORIGINS.includes(origin)) return callback(null, origin);
    return callback(new Error('Not allowed by CORS'));
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  exposedHeaders: ['X-Request-Id'],
  maxAge: 600
}));

Django (Python): django-cors-headers

# settings.py -- pip install django-cors-headers
INSTALLED_APPS = ['corsheaders', ...]
MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # Before CommonMiddleware
    'django.middleware.common.CommonMiddleware',
    ...
]
CORS_ALLOWED_ORIGINS = [
    'https://app.example.com',
    'https://staging.example.com',
]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_METHODS = ['GET','POST','PUT','PATCH','DELETE','OPTIONS']
CORS_ALLOW_HEADERS = ['accept','authorization','content-type']
CORS_PREFLIGHT_MAX_AGE = 600

Go: rs/cors middleware

package main
import (
    "net/http"
    "github.com/rs/cors"  // v1.11+
)
func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/data", handler)
    c := cors.New(cors.Options{
        AllowedOrigins:   []string{"https://app.example.com"},
        AllowedMethods:   []string{"GET","POST","PUT","DELETE"},
        AllowedHeaders:   []string{"Content-Type","Authorization"},
        ExposedHeaders:   []string{"X-Request-Id"},
        AllowCredentials: true,
        MaxAge:           600,
    })
    http.ListenAndServe(":8080", c.Handler(mux))
}

Spring Boot (Java): Global CORS config

@Configuration
public class CorsConfig {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**")
                    .allowedOrigins("https://app.example.com")
                    .allowedMethods("GET","POST","PUT","DELETE")
                    .allowedHeaders("Content-Type","Authorization")
                    .exposedHeaders("X-Request-Id")
                    .allowCredentials(true)
                    .maxAge(600);
            }
        };
    }
}

Anti-Patterns

Wrong: Reflecting Origin header without validation

// BAD -- reflects any origin, creating a universal CORS bypass
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  next();
});

Correct: Validate Origin against allowlist

// GOOD -- only allows known origins
const ALLOWED = new Set(['https://app.example.com']);
app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (ALLOWED.has(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    res.setHeader('Vary', 'Origin');
  }
  next();
});

Wrong: Wildcard origin with credentials

// BAD -- browsers reject * with credentials
app.use(cors({ origin: '*', credentials: true }));

Correct: Explicit origin with credentials

// GOOD -- specific origin allows credentialed requests
app.use(cors({ origin: 'https://app.example.com', credentials: true }));

Wrong: Allowing the null origin

# BAD -- null origin can be forged by sandboxed iframes
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true

Correct: Only allow real HTTPS origins

# GOOD -- explicit HTTPS origin only
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin

Wrong: Missing Vary: Origin header

// BAD -- CDN caches ACAO for one origin, serves to all
res.setHeader('Access-Control-Allow-Origin', validatedOrigin);
// Missing: Vary: Origin

Correct: Always include Vary: Origin

// GOOD -- tells caches response varies by Origin
res.setHeader('Access-Control-Allow-Origin', validatedOrigin);
res.setHeader('Vary', 'Origin');

Wrong: Unanchored regex origin matching

// BAD -- matches evil-example.com or example.com.evil.com
const isAllowed = /example\.com/.test(origin);

Correct: Exact match or anchored regex

// GOOD -- exact match from Set
const ALLOWED = new Set(['https://app.example.com']);
const isAllowed = ALLOWED.has(origin);

Common Pitfalls

Diagnostic Commands

# Check CORS headers for a specific origin
curl -sI -H "Origin: https://app.example.com" https://your-api.com/endpoint \
  | grep -i "access-control\|vary"

# Test preflight (OPTIONS) request
curl -sI -X OPTIONS \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: PUT" \
  -H "Access-Control-Request-Headers: Authorization, Content-Type" \
  https://your-api.com/endpoint

# Verify blocked origin returns no ACAO
curl -sI -H "Origin: https://evil.com" https://your-api.com/endpoint \
  | grep -i access-control

# Check if null origin is allowed (should fail)
curl -sI -H "Origin: null" https://your-api.com/endpoint \
  | grep -i access-control

# Browser: DevTools > Console > filter "CORS"

Version History & Compatibility

Standard/FeatureStatusBrowser SupportKey Change
CORS (Fetch spec)Living StandardAll modern browsersCore CORS mechanism: preflight, ACAO, credentials
Private Network AccessDraftChrome 104+Preflight required for public-to-private network requests
Wildcard * in ACAH/ACAMFetch specChrome 63+, Firefox 69+, Safari 15.4+Wildcard allowed for non-credentialed requests
Access-Control-Max-Age defaultFetch specVariesChrome: 5s default, Firefox: 24h, Safari: 5s
Preflight caching capFetch specAll modernChrome: 7200s max, Firefox: 86400s max

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Frontend and API are on different originsFrontend and API share the same originNo CORS needed -- same-origin policy allows requests
Third-party integrations need your public APIProtecting data from cross-origin readsServer-side authentication/authorization
Mobile web view hitting your API on a different originServer-to-server communication (no browser)API keys or mTLS instead of CORS
CDN serves fonts/images to pages on different originsYou want to restrict which servers call your APICORS does NOT restrict non-browser clients

Important Caveats

Related Units