The essential facts about CORS
CORS (Cross-Origin Resource Sharing) is the browser mechanism that controls cross-origin HTTP requests. The Same-Origin Policy, introduced by Netscape Navigator 2.02 in 1995, blocks scripts from reading resources on different origins (protocol + host + port). CORS emerged as a standardized bypass mechanism, becoming a W3C Recommendation in January 2014 and now living in the WHATWG Fetch specification. Critically, CORS only prevents reading responses—cross-origin requests still execute, meaning CORS is not CSRF protection.
Technical fundamentals
The origin model
Two URLs share an origin only if all three components match: protocol (http:// vs https://), host (example.com vs api.example.com), and port (:80 vs :443). This means http://localhost:3000 and http://localhost:5000 are different origins—a common source of developer confusion.
Simple vs preflighted requests
A request is “simple” (no preflight) only if it uses GET, HEAD, or POST with only CORS-safelisted headers (Accept, Accept-Language, Content-Language, Content-Type with restrictions) and Content-Type limited to application/x-www-form-urlencoded, multipart/form-data, or text/plain. All JSON requests trigger preflight because application/json is not safelisted—this surprises many developers.
The preflight flow
- Browser sends OPTIONS with
Origin,Access-Control-Request-Method, andAccess-Control-Request-Headers - Server must respond with
Access-Control-Allow-Origin,Access-Control-Allow-Methods, andAccess-Control-Allow-Headers - Browser caches result per
Access-Control-Max-Age(default: 5 seconds; Chrome max: 2 hours; Firefox max: 24 hours) - Actual request proceeds only if preflight succeeds
All CORS headers explained
| Response Header | Purpose | Key Behaviors |
|---|---|---|
Access-Control-Allow-Origin | Allowed origins | Single value only; use Vary: Origin with dynamic origins; * forbidden with credentials |
Access-Control-Allow-Methods | Allowed HTTP methods | Return in preflight response |
Access-Control-Allow-Headers | Allowed request headers | Must include all non-safelisted headers client sends |
Access-Control-Allow-Credentials | Enables cookies/auth | Only value is true; requires explicit origin, not * |
Access-Control-Max-Age | Preflight cache duration | Seconds; reduces OPTIONS overhead |
Access-Control-Expose-Headers | Headers JS can read | Default: only Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, Pragma |
| Request Header (set by browser) | Purpose |
|---|---|
Access-Control-Request-Method | Method for actual request |
Access-Control-Request-Headers | Non-safelisted headers to be used |
Common CORS errors by browser
Chrome error patterns
Chrome uses consistent templates. The most frequent errors:
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.
...Response to preflight request doesn't pass access control check: The value of the
'Access-Control-Allow-Origin' header in the response must not be the wildcard '*'
when the request's credentials mode is 'include'.
...Request header field Authorization is not allowed by Access-Control-Allow-Headers
in preflight response.
Chrome often masks underlying 4xx/5xx errors as CORS errors when Access-Control-Allow-Origin is missing from error responses.
Firefox error patterns
Firefox uses distinctive “Reason:” format with status codes:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote
resource at [URL]. (Reason: CORS header 'Access-Control-Allow-Origin' missing).
Status code: 404.
Firefox provides more verbose errors with specific reason codes like CORSMissingAllowOrigin, CORSNotSupportingCredentials, CORSPreflightDidNotSucceed, and links directly to MDN documentation.
Safari quirks
Safari has unique behaviors causing developer frustration:
- Stricter about including protocol in allowed origins
- WebKit disk cache can omit CORS headers on second load of static resources
- May display CORS errors when underlying issue is different (e.g., 400 Bad Request)
- Intelligent Tracking Prevention (ITP) affects credential scenarios
Real-world trigger scenarios
| Scenario | Cause | Fix |
|---|---|---|
| Different ports | localhost:3000 → localhost:5000 | Configure server CORS |
| localhost vs 127.0.0.1 | Considered different origins | Use consistent hostname |
| HTTP to HTTPS | Protocol mismatch | Use matching protocols |
| Custom headers | Authorization, Content-Type: application/json trigger preflight | Handle OPTIONS request |
| Credentials with wildcard | * + credentials blocked | Use explicit origin |
| Subdomain requests | app.example.com → api.example.com | Allow specific subdomain |
Server-side solutions by platform
Node.js/Express
// Using cors package (recommended)
const cors = require('cors');
app.use(cors({
origin: ['https://example.com', 'https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
}));
// Dynamic origin validation
app.use(cors({
origin: function(origin, callback) {
const allowlist = ['https://example.com', 'https://app.example.com'];
if (!origin || allowlist.includes(origin)) callback(null, true);
else callback(new Error('Not allowed by CORS'));
},
credentials: true
}));
Python (Django, Flask, FastAPI)
# Django (django-cors-headers)
CORS_ALLOWED_ORIGINS = ["https://example.com"]
CORS_ALLOW_CREDENTIALS = True
# Flask (flask-cors)
CORS(app, resources={r"/api/*": {"origins": ["https://example.com"], "supports_credentials": True}})
# FastAPI
app.add_middleware(CORSMiddleware, allow_origins=["https://example.com"],
allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
Nginx
location /api/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://example.com' always;
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;
return 204;
}
add_header 'Access-Control-Allow-Origin' 'https://example.com' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
}
Apache (.htaccess)
<IfModule mod_headers.c>
Header set Access-Control-Allow-Origin "https://example.com"
Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header set Access-Control-Allow-Headers "Content-Type, Authorization"
Header set Access-Control-Allow-Credentials "true"
</IfModule>
AWS (S3, API Gateway, Lambda)
// S3 CORS configuration
[{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
"AllowedOrigins": ["https://example.com"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 86400
}]
// Lambda response
return {
statusCode: 200,
headers: {
"Access-Control-Allow-Origin": "https://example.com",
"Access-Control-Allow-Credentials": "true"
},
body: JSON.stringify(data)
};
Cloudflare Workers
export default {
async fetch(request) {
if (request.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "https://example.com",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
"Access-Control-Allow-Headers": "Content-Type, Authorization"
}
});
}
// Handle actual request...
}
};
ASP.NET Core
builder.Services.AddCors(options => {
options.AddPolicy("ApiPolicy", policy => {
policy.WithOrigins("https://example.com")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
app.UseCors("ApiPolicy");
Spring Boot
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true)
.maxAge(86400);
}
}
Client-side workarounds when you can’t modify the server
Development-only options
Dev server proxies (recommended for local development):
// Vite
server: { proxy: { '/api': { target: 'http://localhost:4000', changeOrigin: true } } }
// Create React App (package.json)
"proxy": "http://localhost:4000"
// Next.js (next.config.js)
async rewrites() { return [{ source: '/api/:path*', destination: 'http://localhost:4000/:path*' }]; }
Browser extensions (CORS Unblock, Allow CORS) - development only, never browse normal websites with these enabled.
Chrome unsafe mode: chrome --disable-web-security --user-data-dir=/tmp/chrome_dev - never use for regular browsing.
CORS proxy services
| Service | Status | Limitations |
|---|---|---|
| corsproxy.io | Active | 1MB limit, text-only (HTML disabled), rate limiting |
| cors-anywhere | Self-host only | Public demo rate-limited since 2021 |
| allorigins.win | Active | URL prefix approach |
| cors.sh | Active | Alternative to cors-anywhere |
Security warning: Third-party proxies can read all traffic. Never use for sensitive data, authentication, or production.
Production solutions
Backend-for-Frontend (BFF) pattern: Create a server-side proxy that:
- Makes API calls from your server (no CORS)
- Uses session cookies with browser (same-origin)
- Keeps access tokens server-side (immune to XSS)
Serverless functions: AWS Lambda, Cloudflare Workers, or Vercel Edge Functions as proxies with your own CORS headers.
Advanced topics and security
Credentials and cookies
Using credentials: 'include' or withCredentials: true requires:
- Server must return
Access-Control-Allow-Credentials: true - Server must return explicit origin (not
*) inAccess-Control-Allow-Origin - Browser’s SameSite cookie settings may still block cookies (Chrome defaults to
SameSite=Laxsince 2020)
WebSockets bypass CORS entirely
WebSockets use WS/WSS protocols, not HTTP, so CORS does not apply. However:
- Browser still sends
Originheader in WebSocket handshake - Server must manually validate Origin or face Cross-Site WebSocket Hijacking (CSWSH)
- Spring Framework defaults to same-origin only since 4.1.5
Security vulnerabilities to avoid
Most dangerous anti-pattern: Reflecting the Origin header without validation:
// DANGEROUS - allows any attacker site to read authenticated responses
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
Origin validation pitfalls:
- Regex
^.*example\.com$matchesattackerexample.com - Suffix matching allows subdomain takeover attacks
Origin: nullcan be generated via sandboxed iframes, cross-origin redirects, or file: protocol
Missing Vary: Origin: Causes cache poisoning when dynamically setting CORS headers.
Private Network Access (CORS-RFC1918)
Chrome 94+ prevents public websites from accessing private network resources (localhost, 192.168.x.x). Requires:
Access-Control-Request-Private-Network: truein preflightAccess-Control-Allow-Private-Network: truein response- HTTPS secure context
SEO and content strategy
Competitive landscape
Top-ranking domains: MDN Web Docs (#1), PortSwigger Web Security Academy (#2), Wikipedia (#3), AWS docs (#4), Auth0 blog (#5). Stack Overflow has 14,600+ CORS questions.
Target keywords
| Category | Keywords |
|---|---|
| Primary | ”CORS error”, “what is CORS”, “Access-Control-Allow-Origin” |
| Long-tail errors | ”No ‘Access-Control-Allow-Origin’ header is present”, “preflight request doesn’t pass access control check” |
| Platform-specific | ”express cors”, “nginx cors”, “react cors error”, “AWS API Gateway CORS” |
| Scenario-based | ”CORS with credentials”, “disable CORS Chrome”, “CORS proxy” |
Content gaps in existing articles
Existing content misses:
- Visual troubleshooting flowcharts - decision trees for diagnosis
- Error message mapping table - exact console error → specific fix
- Environment separation - clear dev vs. production guidance
- Security deep-dive - CORS misconfiguration exploitation
- Multi-framework comparison - side-by-side code snippets
- Interactive tools - CORS testers, error decoders
Recommended article structure
H1: The Complete Guide to CORS Errors
H2: What is CORS and why does it exist?
H3: The Same-Origin Policy explained
H3: How CORS relaxes these restrictions
H2: How CORS works technically
H3: Simple requests vs preflighted requests
H3: The preflight OPTIONS flow (diagram)
H3: All CORS headers explained
H2: Common CORS errors and solutions
H3: "No Access-Control-Allow-Origin header present"
H3: "Preflight request doesn't pass access control check"
H3: "Wildcard * not allowed with credentials"
H3: Why Postman works but browsers don't
H2: Server-side configuration by platform (tabbed code)
H3: Node.js/Express
H3: Python (Django/Flask/FastAPI)
H3: nginx and Apache
H3: AWS (API Gateway, S3, Lambda)
H3: .NET and Spring Boot
H2: Client-side workarounds
H3: Dev server proxies (Vite, CRA, Next.js)
H3: CORS proxy services and their risks
H3: Browser extensions (dev only)
H2: CORS security best practices
H3: Credential handling
H3: Origin validation mistakes
H3: WebSocket considerations
H2: Quick reference tables
- Error → Solution mapping
- Headers cheat sheet
Technical SEO recommendations
- Target word count: 3,500–5,000 words
- Schema markup: TechArticle, HowTo, FAQPage
- Featured snippet optimization: 40–60 word definition for “What is CORS?”
- Code presentation: Tabbed interface, copy buttons, syntax highlighting
- Visuals: CORS flow diagram, preflight sequence diagram, error decision tree (4–6 total)
- Internal linking: Related HTTP security topics
- External links: MDN, WHATWG Fetch spec, RFC references
Quick reference tables
CORS headers cheat sheet
| Header | Type | Required For | Value Example |
|---|---|---|---|
| Access-Control-Allow-Origin | Response | All CORS | https://example.com or * |
| Access-Control-Allow-Methods | Response | Preflight | GET, POST, PUT, DELETE |
| Access-Control-Allow-Headers | Response | Preflight with custom headers | Content-Type, Authorization |
| Access-Control-Allow-Credentials | Response | Cookies/auth | true |
| Access-Control-Max-Age | Response | Caching preflight | 86400 |
| Access-Control-Expose-Headers | Response | Non-safelisted response headers | X-Custom-Header |
Error → solution mapping
| Error Message | Likely Cause | Fix |
|---|---|---|
| No ‘Access-Control-Allow-Origin’ header | Server not sending CORS headers | Add CORS configuration to server |
| Response to preflight doesn’t pass access control check | OPTIONS not handled or missing headers | Return CORS headers on OPTIONS with 204 |
| Wildcard * not allowed with credentials mode ‘include’ | Using credentials: 'include' with * | Use explicit origin |
| Request header field X not allowed | Missing from Access-Control-Allow-Headers | Add header to allowed list |
| Multiple Access-Control-Allow-Origin headers | Duplicate CORS configuration | Configure CORS in only one layer |
Preflight triggers
| Condition | Example | Triggers Preflight? |
|---|---|---|
| GET with only Accept header | fetch('/api') | No |
| POST with form-data | <form enctype="multipart/form-data"> | No |
| POST with application/json | Content-Type: application/json | Yes |
| Any custom header | Authorization, X-API-Key | Yes |
| PUT, DELETE, PATCH methods | fetch('/api', {method: 'DELETE'}) | Yes |