CORS (Cross-Origin Resource Sharing) is a security feature that often confuses developers. This comprehensive guide explains what CORS is, why it exists, and how to fix common CORS errors.
Table of Contents
- What is CORS?
- The Same-Origin Policy
- How CORS Works
- Common CORS Errors
- Fixing CORS Issues
- CORS in Different Frameworks
- Security Best Practices
What is CORS?
CORS (Cross-Origin Resource Sharing) is a security mechanism that allows or restricts web pages from making requests to a different domain than the one that served the web page.
Simple Explanation: Imagine you're at a restaurant (website A) and you want to order food from a different restaurant (website B). CORS is like asking permission: "Can I order from that other restaurant?" If they say yes, you can. If they say no, you can't.
Why CORS Exists
CORS exists to protect you from malicious websites. Without CORS, any website could:
- Read your emails from Gmail
- Transfer money from your bank account
- Post to your social media
- Access your private data from other sites
WITHOUT CORS (Dangerous):
evil-site.com → Can read your gmail.com emails
evil-site.com → Can access your bank.com account
evil-site.com → Can post to your facebook.com
WITH CORS (Safe):
evil-site.com → ❌ Blocked by Gmail
evil-site.com → ❌ Blocked by Bank
evil-site.com → ❌ Blocked by Facebook
Only authorized sites can access your data!
Real-World Example
// Your website: https://myapp.com
// API server: https://api.example.com
// This request will trigger CORS check:
fetch('https://api.example.com/users')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error('CORS Error:', err));
// Browser thinks: "myapp.com wants to access api.example.com"
// Browser checks: "Does api.example.com allow requests from myapp.com?"
// If YES → Request succeeds ✅
// If NO → CORS error ❌
The Same-Origin Policy
Before understanding CORS, you need to understand the Same-Origin Policy.
What is an Origin?
An origin consists of three parts:
- Protocol (http vs https)
- Domain (example.com)
- Port (80, 443, 3000, etc.)
https://example.com:443/page
└─┬──┘ └────┬─────┘└┬┘
Protocol Domain Port
All three must match for same origin!
Same-Origin Examples
Base URL: https://example.com/page
SAME ORIGIN (✅ Allowed):
https://example.com/about ← Same everything
https://example.com/api/users ← Same everything
https://example.com:443/login ← Port 443 is default for HTTPS
DIFFERENT ORIGIN (❌ Blocked without CORS):
http://example.com/page ← Different protocol (http vs https)
https://api.example.com/page ← Different subdomain
https://example.com:8080/page ← Different port
https://another-site.com/page ← Different domain
Why Same-Origin Policy?
// Scenario: You're logged into bank.com
// Evil site tries to access your data:
// evil-site.com page contains:
fetch('https://bank.com/api/account')
.then(res => res.json())
.then(data => {
// Send victim's bank data to attacker
sendToAttacker(data);
});
// Without Same-Origin Policy:
// ❌ This would work! Evil site gets your bank data!
// With Same-Origin Policy:
// ✅ Browser blocks this request
// Error: "Blocked by CORS policy"
Important: The Same-Origin Policy is enforced by the BROWSER, not the server. The request still reaches the server, but the browser blocks the response from being read by JavaScript.
How CORS Works
CORS uses HTTP headers to allow servers to specify which origins can access their resources.
Simple CORS Request
For simple GET requests, the browser adds an Origin header:
# Browser sends:
GET /api/users HTTP/1.1
Host: api.example.com
Origin: https://myapp.com
# Server responds:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.com
Content-Type: application/json
{"users": [...]}
# Browser checks: "Does Access-Control-Allow-Origin match my origin?"
# YES → Allow the response ✅
# NO → Block the response ❌
Preflight Request
For complex requests (POST, PUT, DELETE, custom headers), the browser sends a preflight request first:
# Step 1: Browser sends OPTIONS request (preflight)
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://myapp.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
# Step 2: Server responds with permissions
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
# Step 3: If allowed, browser sends actual request
POST /api/users HTTP/1.1
Host: api.example.com
Origin: https://myapp.com
Content-Type: application/json
{"name": "John Doe"}
CORS Flow Diagram
CLIENT (myapp.com) BROWSER SERVER (api.example.com)
│ │ │
│ fetch('api/users') │ │
├───────────────────────>│ │
│ │ │
│ │ OPTIONS /api/users │
│ │ Origin: myapp.com │
│ ├───────────────────────────>│
│ │ │
│ │ Access-Control-Allow- │
│ │ Origin: myapp.com │
│ │<───────────────────────────┤
│ │ │
│ │ GET /api/users │
│ ├───────────────────────────>│
│ │ │
│ │ Response + CORS headers │
│ │<───────────────────────────┤
│ │ │
│ Response (if allowed) │ │
│<───────────────────────┤ │
Key CORS Headers
Request Headers (sent by browser):
Origin: https://myapp.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
Response Headers (sent by server):
# Who can access?
Access-Control-Allow-Origin: https://myapp.com
# or
Access-Control-Allow-Origin: *
# Which methods are allowed?
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
# Which headers are allowed?
Access-Control-Allow-Headers: Content-Type, Authorization
# Can send cookies?
Access-Control-Allow-Credentials: true
# Cache preflight for how long? (seconds)
Access-Control-Max-Age: 86400
Common CORS Errors
Error 1: No 'Access-Control-Allow-Origin' Header
❌ ERROR:
Access to fetch at 'https://api.example.com/users' from origin
'https://myapp.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the
requested resource.
CAUSE: Server didn't send CORS headers
FIX: Add CORS headers on server
Error 2: Origin Not Allowed
❌ ERROR:
Access to fetch at 'https://api.example.com/users' from origin
'https://myapp.com' has been blocked by CORS policy:
The 'Access-Control-Allow-Origin' header has a value
'https://other-site.com' that is not equal to the supplied origin.
CAUSE: Server allows other-site.com but not myapp.com
FIX: Add myapp.com to allowed origins on server
Error 3: Credentials Flag Mismatch
❌ ERROR:
Access to fetch at 'https://api.example.com/users' from origin
'https://myapp.com' has been blocked by CORS policy:
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'.
CAUSE: Using credentials with wildcard (*)
FIX: Specify exact origin instead of *
Error 4: Method Not Allowed
❌ ERROR:
Access to fetch at 'https://api.example.com/users' from origin
'https://myapp.com' has been blocked by CORS policy:
Method PUT is not allowed by Access-Control-Allow-Methods
in preflight response.
CAUSE: Server doesn't allow PUT method
FIX: Add PUT to Access-Control-Allow-Methods
Error 5: Header Not Allowed
❌ ERROR:
Access to fetch at 'https://api.example.com/users' from origin
'https://myapp.com' has been blocked by CORS policy:
Request header field authorization is not allowed by
Access-Control-Allow-Headers in preflight response.
CAUSE: Server doesn't allow Authorization header
FIX: Add Authorization to Access-Control-Allow-Headers
Fixing CORS Issues
Solution 1: Enable CORS on Server
The proper solution is to configure CORS on the server.
Node.js (Express)
const express = require('express');
const cors = require('cors');
const app = express();
// Option 1: Enable CORS for all routes (simple)
app.use(cors());
// Option 2: Enable CORS for specific origin
app.use(cors({
origin: 'https://myapp.com'
}));
// Option 3: Multiple origins
app.use(cors({
origin: ['https://myapp.com', 'https://otherapp.com']
}));
// Option 4: Dynamic origin (function)
app.use(cors({
origin: function(origin, callback) {
const allowedOrigins = ['https://myapp.com', 'https://otherapp.com'];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
}));
// Option 5: Full configuration
app.use(cors({
origin: 'https://myapp.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
}));
app.listen(3000);
Manual CORS Headers (Node.js)
// Without cors package - manual headers
app.use((req, res, next) => {
// Allow specific origin
res.header('Access-Control-Allow-Origin', 'https://myapp.com');
// Allow multiple origins
const allowedOrigins = ['https://myapp.com', 'https://otherapp.com'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
}
// Allow methods
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
// Allow headers
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Allow credentials
res.header('Access-Control-Allow-Credentials', 'true');
// Handle preflight
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
Solution 2: Development Proxy
For development only, use a proxy to avoid CORS:
Create React App
// package.json
{
"name": "my-app",
"proxy": "http://localhost:3001"
}
// Now requests go through proxy (same origin)
fetch('/api/users') // Actually goes to http://localhost:3001/api/users
Vite
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
rewrite: (path) => path.replace(/^\\/api/, '')
}
}
}
}
Webpack Dev Server
// webpack.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
}
}
}
Solution 3: Browser Extension (Development Only)
Warning: Only use browser extensions for local development. Never rely on them for production!
Chrome Extension: "CORS Unblock" or "Allow CORS"
⚠️ Only for testing!
⚠️ Disables important security
⚠️ Never use in production
⚠️ Always fix CORS properly on server
Solution 4: JSONP (Legacy)
// JSONP (old technique, avoid if possible)
// Only works with GET requests
// Not secure, don't use for sensitive data
function jsonp(url, callback) {
const script = document.createElement('script');
script.src = \`\${url}?callback=\${callback}\`;
document.head.appendChild(script);
}
// Server must respond with:
// callbackName({"data": "value"});
// Modern alternative: Use CORS instead!
CORS in Different Frameworks
Python (Flask)
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
# Enable CORS for all routes
CORS(app)
# Or specific origin
CORS(app, origins=['https://myapp.com'])
# Or with options
CORS(app,
origins=['https://myapp.com'],
methods=['GET', 'POST', 'PUT', 'DELETE'],
allow_headers=['Content-Type', 'Authorization'])
@app.route('/api/users')
def get_users():
return {'users': []}
Python (Django)
# Install: pip install django-cors-headers
# settings.py
INSTALLED_APPS = [
...
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
...
]
# Allow all origins (development)
CORS_ALLOW_ALL_ORIGINS = True
# Or specific origins (production)
CORS_ALLOWED_ORIGINS = [
'https://myapp.com',
'https://otherapp.com',
]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_METHODS = [
'DELETE',
'GET',
'OPTIONS',
'PATCH',
'POST',
'PUT',
]
PHP
<?php
// Set CORS headers
header('Access-Control-Allow-Origin: https://myapp.com');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Credentials: true');
// Handle preflight
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
// Your API logic here
?>
Ruby on Rails
# Gemfile
gem 'rack-cors'
# config/application.rb
config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'https://myapp.com'
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options],
credentials: true
end
end
ASP.NET Core
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("AllowMyApp",
builder =>
{
builder.WithOrigins("https://myapp.com")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
}
public void Configure(IApplicationBuilder app)
{
app.UseCors("AllowMyApp");
app.UseRouting();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
Nginx
server {
listen 80;
server_name api.example.com;
location / {
# Simple CORS
add_header Access-Control-Allow-Origin https://myapp.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-Allow-Credentials true always;
# Handle preflight
if ($request_method = 'OPTIONS') {
return 204;
}
# Proxy to backend
proxy_pass http://localhost:3000;
}
}
Apache (.htaccess)
# Enable CORS
Header set Access-Control-Allow-Origin "https://myapp.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"
# Handle preflight
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=200,L]
Security Best Practices
1. Never Use Wildcard in Production with Credentials
// ❌ DANGEROUS - Anyone can access with credentials
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Credentials', 'true');
// ✅ SAFE - Specific origin with credentials
res.header('Access-Control-Allow-Origin', 'https://myapp.com');
res.header('Access-Control-Allow-Credentials', 'true');
2. Validate Origins
// ❌ BAD - Allows any origin
app.use(cors({
origin: true // Dangerous!
}));
// ✅ GOOD - Whitelist specific origins
const allowedOrigins = [
'https://myapp.com',
'https://app.mycompany.com'
];
app.use(cors({
origin: function(origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
}));
3. Limit Methods and Headers
// ❌ BAD - Allow everything
app.use(cors({
methods: '*',
allowedHeaders: '*'
}));
// ✅ GOOD - Only what you need
app.use(cors({
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
4. Use Environment Variables
// .env
ALLOWED_ORIGINS=https://myapp.com,https://app.mycompany.com
// server.js
const allowedOrigins = process.env.ALLOWED_ORIGINS.split(',');
app.use(cors({
origin: allowedOrigins,
credentials: true
}));
5. Different Settings for Dev vs Production
// Development - Allow localhost
if (process.env.NODE_ENV === 'development') {
app.use(cors({
origin: ['http://localhost:3000', 'http://localhost:3001'],
credentials: true
}));
}
// Production - Strict origins
if (process.env.NODE_ENV === 'production') {
app.use(cors({
origin: ['https://myapp.com'],
credentials: true
}));
}
Common Misconceptions
Myth 1: "CORS is a Server Security Feature"
❌ WRONG: CORS protects the server
✅ RIGHT: CORS protects the USER
CORS is enforced by the BROWSER to protect users from
malicious websites. The server still receives the request!
Myth 2: "I Can't Test APIs Without Fixing CORS"
❌ WRONG: Can't test until CORS is fixed
✅ RIGHT: Can test in many ways
Tools that bypass CORS (for testing):
✅ Postman
✅ curl
✅ Thunder Client (VS Code)
✅ HTTPie
✅ Server-to-server requests (no browser)
Myth 3: "Using a Proxy Fixes CORS"
❌ WRONG: Proxy fixes CORS permanently
✅ RIGHT: Proxy works only in development
Development proxy: ✅ Works (same origin)
Production: ❌ Still need to configure CORS on server
Myth 4: "CORS Prevents API Calls"
❌ WRONG: CORS blocks the API call
✅ RIGHT: CORS blocks reading the RESPONSE
The request DOES reach the server.
CORS only prevents JavaScript from reading the response.
Debugging CORS Issues
Step 1: Check Browser Console
Look for error messages like:
"Access to fetch... has been blocked by CORS policy"
Error will tell you exactly what's wrong:
- Missing header?
- Wrong origin?
- Method not allowed?
- Header not allowed?
Step 2: Check Network Tab
1. Open DevTools (F12)
2. Go to Network tab
3. Look for your request
4. Check Response Headers:
- Access-Control-Allow-Origin: ?
- Access-Control-Allow-Methods: ?
- Access-Control-Allow-Headers: ?
5. If OPTIONS request (preflight):
- Should return 200 or 204
- Should have all Access-Control-* headers
Step 3: Test with Postman
If request works in Postman but fails in browser:
→ It's definitely a CORS issue
→ Server needs to send CORS headers
If request fails in both Postman and browser:
→ Not a CORS issue
→ Check server code, authentication, etc.
Step 4: Verify Server Configuration
// Add logging to see what's happening
app.use((req, res, next) => {
console.log('Origin:', req.headers.origin);
console.log('Method:', req.method);
console.log('Headers:', req.headers);
next();
});
Summary
CORS is a browser security feature that protects users by controlling which websites can access resources from other origins. While it can be confusing at first, understanding CORS is essential for modern web development.
Key Takeaways:
✅ CORS = Cross-Origin Resource Sharing
✅ Enforced by BROWSER, not server
✅ Protects USERS from malicious websites
✅ Same-Origin = same protocol + domain + port
✅ Server must send Access-Control-Allow-Origin header
✅ Complex requests trigger preflight (OPTIONS)
✅ Fix CORS on the SERVER, not client
✅ Never use * with credentials in production
✅ Test with Postman to verify it's a CORS issue
✅ Use proxy in development, proper CORS in production
Quick Fix Checklist:
- ✅ Add CORS middleware to server
- ✅ Specify allowed origins (not
*in production) - ✅ Allow needed methods and headers
- ✅ Handle OPTIONS requests (preflight)
- ✅ Test from browser and Postman