Hey everyone! Welcome back to another deep dive into web development concepts. Today we’re tackling one of the most misunderstood and frustrating topics for web developers: CORS (Cross-Origin Resource Sharing). If you’ve ever worked with APIs and encountered that dreaded CORS error, this comprehensive guide will not only help you understand what’s happening but also why it’s absolutely crucial for web security.
By the end of this article, you’ll have a complete understanding of CORS from a security perspective, how to configure it correctly, and most importantly, why this concept even exists in the first place. Let’s dive in!
Before we dive into CORS, let’s establish the fundamental web architecture that we’re all familiar with. In any web application, you have two primary components:
The Client#
This could be:
A web page running in a browser
A React application
An Angular or Vue.js app
Any client-side JavaScript application
The Server#
This is your backend that:
Processes requests
Manages data
Returns responses
Handles business logic
The interaction between these components follows a simple request-response cycle:
Client sends a request to the server
Server processes the request
Server returns a response with the requested data
This is the foundation of how web applications work, and there’s nothing problematic about this basic interaction when both the client and server are on the same domain.
Now, let’s imagine a world without CORS restrictions. To understand why CORS exists, we need to visualize the security catastrophe that would unfold without it.
Scenario 1: The Social Media Attack#
Picture this realistic scenario:
You have your website hosted on
https://yoursite.devYou open your site in one browser tab
In another tab, you’re logged into Facebook (
facebook.com)Facebook has set authentication cookies and tokens in your browser
Now, here’s the terrifying part. Without CORS, if I included this JavaScript code on my website:
// This would be CATASTROPHIC without CORS
fetch("https://facebook.com/api/friends")
.then((response) => response.json())
.then((friends) => {
// I now have access to your Facebook friends!
console.log("Stolen friends data:", friends);
sendToMyServer(friends); // Send to attacker's server
});
What would happen?
Your browser would send the request to Facebook
Since you’re logged in, your authentication cookies would be included
Facebook’s server would see valid authentication
Your friends list would be returned to my malicious website
I could then steal, store, or misuse this data
But it gets worse. I wouldn’t just be limited to reading data. I could also:
// Create posts on your behalf
fetch("https://facebook.com/api/posts", {
method: "POST",
body: JSON.stringify({
message: "This post was made without my knowledge!",
}),
});
// Reset your password
fetch("https://facebook.com/api/reset-password", {
method: "POST",
body: JSON.stringify({
newPassword: "hackedPassword123",
}),
});
Scenario 2: The Banking Disaster#
The implications become even more severe with financial services:
You’re logged into your bank (
hdfc.com) in one tabYou visit a malicious website in another tab
That website executes this code:
// Without CORS, this would expose your banking data
fetch("https://hdfc.com/api/balance")
.then((response) => response.json())
.then((balance) => {
// Attacker now knows your account balance
console.log("Bank balance:", balance);
});
// Even worse - unauthorized transactions
fetch("https://hdfc.com/api/transfer", {
method: "POST",
body: JSON.stringify({
amount: 10000,
toAccount: "attackerAccount123",
}),
});
The Core Problem: Cross-Origin Requests#
These scenarios demonstrate cross-origin requests - when one origin (domain) tries to access resources from a different origin. Here’s what’s happening:
yoursite.dev(Origin A) making requests tofacebook.com(Origin B)malicioussite.com(Origin A) making requests tohdfc.com(Origin B)
Without proper controls, any website could interact with any other website using your stored credentials, leading to complete security chaos.
To understand how CORS prevents these attacks, we first need to understand what constitutes an “origin” in web security terms.
The Origin Tuple#
From a browser’s perspective, an origin is defined by a tuple (combination) of three components:
Scheme - The protocol (http or https)
Host - The domain name (like yoursite.dev)
Port - The port number (like 443, 8080, 3000)
Origin Examples#
Let’s look at various examples to understand this better:
Origin: https://yoursite.dev:443
Scheme: https
Host: yoursite.dev
Port: 443
Same Origins:
https://yoursite.dev:443/homehttps://yoursite.dev:443/api/usershttps://yoursite.dev:443/dashboard/settings
Notice that paths don’t matter - only the scheme, host, and port combination determines the origin.
Different Origins:
https://yoursite.dev:443 ← Origin 1
https://api.yoursite.dev:443 ← Origin 2 (different host)
http://yoursite.dev:443 ← Origin 3 (different scheme)
https://yoursite.dev:8080 ← Origin 4 (different port)
Why This Matters#
Understanding origins is crucial because browsers use this definition to determine whether a request is “same-origin” (safe by default) or “cross-origin” (requires special permission through CORS).
The Same-Origin Policy is the foundational security mechanism that browsers implement by default. Let’s explore this in detail.
What is Same-Origin Policy?#
According to Mozilla’s documentation, the Same-Origin Policy states:
“A web application using those APIs can only request resources from the same origin the application was loaded from.”
This means:
yoursite.devcan only communicate with servers onyoursite.devIt cannot communicate with
facebook.comIt cannot communicate with
hdfc.comIt cannot even communicate with
api.yoursite.dev(different subdomain)
What’s Allowed Under Same-Origin Policy#
If your application is hosted on https://yoursite.dev, these requests are allowed:
// ✅ Same origin - allowed
fetch("/api/users");
fetch("/api/posts");
fetch("https://yoursite.dev/api/data");
What’s Blocked Under Same-Origin Policy#
These requests would be blocked:
// ❌ Different origins - blocked
fetch("https://api.yoursite.dev/data"); // Different subdomain
fetch("https://facebook.com/api/friends"); // Different domain
fetch("http://yoursite.dev/api/data"); // Different scheme
fetch("https://yoursite.dev:8080/api"); // Different port
The Problem with Strict Same-Origin Policy#
While this policy provides excellent security, it creates a practical problem in modern web development:
Scenario: Your frontend is hosted on yoursite.dev but your API is hosted on api.yoursite.dev for better architecture and scalability.
Problem: Your own frontend can’t communicate with your own API because they’re different origins!
This is where CORS comes to the rescue by providing a controlled way to relax the same-origin policy.
CORS provides a mechanism for servers to explicitly declare which origins they trust, giving fine-grained control over cross-origin access. Let’s understand this step-by-step process.
The CORS Request Flow#
When you make a cross-origin request, here’s exactly what happens:
Step 1: Browser Adds Origin Header#
When your JavaScript code makes a cross-origin request:
// From https://yoursite.dev
fetch("https://api.yoursite.dev/data");
The browser automatically adds an Origin header to the request:
GET /data HTTP/1.1
Host: api.yoursite.dev
Origin: https://yoursite.dev
Step 2: Server Examines the Origin#
Your server receives the request and sees:
The request is coming from
https://yoursite.devThe request is being made to
https://api.yoursite.devThis is a cross-origin request (different subdomains)
Step 3: Server Makes a Trust Decision#
The server must decide: “Do I trust https://yoursite.dev?”
If YES - Server includes a special response header:
Access-Control-Allow-Origin: https://yoursite.dev
If NO - Server omits this header or sends a different origin.
Step 4: Browser Enforces the Decision#
When the browser receives the response:
If the CORS header is present and matches:
// ✅ Request succeeds
fetch("https://api.yoursite.dev/data")
.then((response) => response.json())
.then((data) => console.log(data)); // This works!
If the CORS header is missing or doesn’t match:
// ❌ Browser throws CORS error
// "Access to fetch at 'https://api.yoursite.dev/data' from origin
// 'https://yoursite.dev' has been blocked by CORS policy"
The Key CORS Headers#
The most important CORS header is:
Access-Control-Allow-Origin: <origin>
Examples:
Access-Control-Allow-Origin: https://yoursite.dev
Access-Control-Allow-Origin: https://app.yoursite.dev
Access-Control-Allow-Origin: *
Let’s build a practical example to see CORS in action. I’ll show you a complete setup with both a server and client.
Server Setup (Express.js on Port 8000)#
// server/index.js
const express = require("express");
const app = express();
const PORT = 8000;
app.use(express.json());
// Route WITHOUT CORS headers
app.get("/data", (req, res) => {
res.json({
message: "Hello from server!",
timestamp: new Date().toISOString(),
});
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Client Setup (Vite React on Port 5173)#
// client/src/App.jsx
import { useState } from "react";
function App() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const fetchData = async () => {
try {
const response = await fetch("http://localhost:8000/data");
const result = await response.json();
setData(result);
setError(null);
} catch (err) {
setError(err.message);
setData(null);
}
};
return (
<div>
<h1>CORS Demo</h1>
<button onClick={fetchData}>Fetch Data</button>
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
{error && <div style={{ color: "red" }}>{error}</div>}
</div>
);
}
export default App;
Testing the CORS Error#
Let’s start both servers:
# Terminal 1 - Start server
cd server
node index.js
# Server running on http://localhost:8000
# Terminal 2 - Start client
cd client
npm run dev
# Client running on http://localhost:5173
Now when you click “Fetch Data”, you’ll see this CORS error in the browser console:
Access to fetch at 'http://localhost:8000/data' from origin 'http://localhost:5173'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.
Analyzing the Failed Request#
Let’s examine what happened in the browser’s Network tab:
Request Headers:
GET /data HTTP/1.1
Host: localhost:8000
Origin: http://localhost:5173
Response Headers:
Content-Type: application/json
Content-Length: 67
Date: Sun, 21 Sep 2025 12:30:00 GMT
# Notice: NO Access-Control-Allow-Origin header!
The browser sent the origin (http://localhost:5173) but the server didn’t respond with the required CORS header, so the browser blocked the request.
Fixing the CORS Issue#
Now let’s modify our server to allow cross-origin requests:
// server/index.js - Updated with CORS
const express = require("express");
const app = express();
const PORT = 8000;
app.use(express.json());
app.get("/data", (req, res) => {
// Add CORS header to allow requests from our frontend
res.setHeader("Access-Control-Allow-Origin", "http://localhost:5173");
res.json({
message: "Hello from server!",
timestamp: new Date().toISOString(),
});
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Restart the server and try the request again. Now it works!
Updated Response Headers:
Access-Control-Allow-Origin: http://localhost:5173
Content-Type: application/json
Content-Length: 67
Date: Sun, 21 Sep 2025 12:30:00 GMT
Important Observations#
Server-side control: The server decides which origins to trust
Exact matching: The header value must exactly match the requesting origin
Per-route basis: In this example, only the
/dataroute has CORS enabledBrowser caching: Browsers may cache CORS preflight responses
The wildcard (*) in CORS headers is both powerful and dangerous. Let’s explore when to use it and when to avoid it.
Using the Wildcard#
You can allow all origins using the wildcard:
app.get("/public-data", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.json({ message: "This is public data available to everyone" });
});
With this setting:
Any website can make requests to your server
Any origin will be allowed
No authentication should be involved
When Wildcards Are Appropriate#
The wildcard is suitable for:
Public APIs that serve public data
// Weather API - public data
app.get("/api/weather/:city", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.json({
city: req.params.city,
temperature: "25°C",
condition: "Sunny",
});
});
Content Delivery Networks (CDNs)
// Serving static assets
app.get("/assets/*", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
// Serve CSS, JS, images, etc.
});
Open-source libraries or widgets
// JavaScript widget that can be embedded anywhere
app.get("/widget.js", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Content-Type", "application/javascript");
// Return widget code
});
The Danger of Wildcards#
Using wildcards becomes extremely dangerous when dealing with authenticated users:
// ❌ DANGEROUS - Don't do this!
app.get("/user/profile", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
// This exposes user data to ANY website!
res.json({
name: "John Doe",
email: "john@example.com",
bankBalance: "$10,000",
});
});
With this configuration:
Any malicious website can request user data
Your users’ private information is exposed to anyone
No control over who can access sensitive endpoints
When working with authentication (cookies, authorization headers, etc.), CORS has additional restrictions that provide an extra layer of security.
Understanding Credentials in HTTP Requests#
Credentials include:
Cookies (session cookies, auth cookies)
Authorization headers (Bearer tokens, Basic auth)
Client-side certificates
Making Requests with Credentials#
By default, cross-origin requests don’t include credentials. To include them, you must explicitly specify:
// Include credentials in the request
fetch("http://localhost:8000/protected-data", {
credentials: "include", // This sends cookies and auth headers
})
.then((response) => response.json())
.then((data) => console.log(data));
Server-side Requirements for Credentialed Requests#
When a request includes credentials, the server has stricter requirements:
app.get("/protected-data", (req, res) => {
// ❌ This will FAIL with credentialed requests
res.setHeader("Access-Control-Allow-Origin", "*");
// ✅ Must specify exact origin for credentialed requests
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
res.setHeader("Access-Control-Allow-Credentials", "true");
res.json({ sensitiveData: "Only for authenticated users" });
});
Why This Restriction Exists#
This restriction prevents the following attack scenario:
// Malicious website trying to steal authenticated data
fetch("https://yourbank.com/api/balance", {
credentials: "include", // Includes your banking session cookies
})
.then((response) => response.json())
.then((balance) => {
// Without the wildcard restriction, this would work!
sendToAttacker(balance);
});
The restriction ensures that:
Only explicitly trusted origins can make credentialed requests
Servers must consciously decide which origins to trust with authenticated data
Blanket permissions aren’t possible for sensitive operations
Complete Example with Authentication#
Here’s a complete example showing proper credential handling:
// Server setup for authenticated requests
app.use(
session({
secret: "your-secret-key",
resave: false,
saveUninitialized: true,
cookie: { secure: false }, // Set to true in production with HTTPS
})
);
// Login endpoint
app.post("/login", (req, res) => {
const { username, password } = req.body;
// Verify credentials (simplified)
if (username === "user" && password === "pass") {
req.session.userId = "user123";
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
res.setHeader("Access-Control-Allow-Credentials", "true");
res.json({ success: true });
} else {
res.status(401).json({ error: "Invalid credentials" });
}
});
// Protected endpoint
app.get("/user/profile", (req, res) => {
if (!req.session.userId) {
return res.status(401).json({ error: "Not authenticated" });
}
// MUST specify exact origin for credentialed requests
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
res.setHeader("Access-Control-Allow-Credentials", "true");
res.json({
userId: req.session.userId,
profile: {
name: "John Doe",
email: "john@example.com",
},
});
});
Client-side usage:
// Login with credentials
fetch("http://localhost:8000/login", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: "user",
password: "pass",
}),
});
// Access protected resource
fetch("http://localhost:8000/user/profile", {
credentials: "include", // Include session cookie
})
.then((response) => response.json())
.then((profile) => console.log(profile));
Not all HTTP requests are treated equally by CORS. Simple requests are sent directly, while complex requests trigger a preflight process.
Simple vs Complex Requests#
Simple Requests (sent directly):
Methods:
GET,HEAD,POSTHeaders: Only simple headers like
Accept,Content-Type(with restrictions)Content-Type: Only
application/x-www-form-urlencoded,multipart/form-data, ortext/plain
Complex Requests (require preflight):
Methods:
PUT,PATCH,DELETE,OPTIONS, etc.Custom headers:
Authorization,X-Custom-Header, etc.Content-Type:
application/json,application/xml, etc.
The Preflight Process#
When you make a complex request, the browser follows this two-step process:
Step 1: Preflight Request (OPTIONS)#
// Your code makes this request
fetch("http://localhost:8000/users/123", {
method: "DELETE",
headers: {
Authorization: "Bearer token123",
},
});
But the browser first sends an OPTIONS request:
OPTIONS /users/123 HTTP/1.1
Host: localhost:8000
Origin: http://localhost:3000
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization
Step 2: Server Preflight Response#
The server must respond with allowed methods and headers:
app.options("/users/:id", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.setHeader("Access-Control-Max-Age", "86400"); // Cache for 24 hours
res.sendStatus(200);
});
Step 3: Actual Request (if preflight succeeds)#
Only if the preflight succeeds does the browser send the actual request:
DELETE /users/123 HTTP/1.1
Host: localhost:8000
Origin: http://localhost:3000
Authorization: Bearer token123
Complete Preflight Example#
Here’s a complete server setup handling preflight requests:
const express = require("express");
const app = express();
app.use(express.json());
// Handle preflight requests for all routes
app.options("*", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PUT, PATCH, DELETE, OPTIONS"
);
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, X-Requested-With"
);
res.setHeader("Access-Control-Max-Age", "86400");
res.sendStatus(200);
});
// Actual API endpoints
app.get("/users", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
res.json([
{ id: 1, name: "John" },
{ id: 2, name: "Jane" },
]);
});
app.post("/users", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
res.status(201).json({ id: 3, ...req.body });
});
app.put("/users/:id", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
res.json({ id: req.params.id, ...req.body });
});
app.delete("/users/:id", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
res.sendStatus(204);
});
app.listen(8000);
Client-side testing:
// This will trigger preflight due to DELETE method
fetch("http://localhost:8000/users/123", {
method: "DELETE",
});
// This will trigger preflight due to JSON content-type
fetch("http://localhost:8000/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "New User" }),
});
// This will trigger preflight due to Authorization header
fetch("http://localhost:8000/users", {
method: "GET",
headers: {
Authorization: "Bearer token123",
},
});
Preflight Optimization#
Preflight requests add network overhead. You can optimize them by:
Caching preflight responses:
res.setHeader("Access-Control-Max-Age", "86400"); // Cache for 24 hours
Using simple requests when possible:
// Instead of this (triggers preflight):
fetch("/api/data", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
// Use this (simple request):
const formData = new FormData();
formData.append("data", JSON.stringify(data));
fetch("/api/data", {
method: "POST",
body: formData,
});
One of the most common questions developers ask is: “Why does my API work in Postman but fail in the browser?” The answer lies in understanding that CORS is exclusively a browser security feature.
CORS is Browser-Only#
CORS restrictions are only enforced by browsers. Here’s why:
Browser Environment#
Shared resource: Multiple websites open in different tabs
Stored credentials: Cookies, tokens, session data from various sites
User’s personal data: Banking, social media, email accounts
Security risk: Malicious sites could access other sites’ data
Server-to-Server Environment#
Isolated environment: Each request is independent
No shared state: No cookies or stored credentials from other sources
Controlled environment: Server administrators control what requests are made
No cross-contamination: One API call can’t accidentally access another service’s data
Practical Examples#
Postman Request (No CORS)#
# This works perfectly - no CORS restrictions
curl -X GET "http://localhost:8000/data" \
-H "Content-Type: application/json"
Browser Request (CORS Applied)#
// This fails without proper CORS headers
fetch("http://localhost:8000/data")
.then((response) => response.json())
.then((data) => console.log(data));
// Error: blocked by CORS policy
Server-to-Server Request (No CORS)#
// Node.js server making request to another server
const axios = require("axios");
async function fetchDataFromAPI() {
try {
const response = await axios.get("http://other-server:8000/data");
console.log(response.data); // This works fine
} catch (error) {
console.error(error);
}
}
Why This Distinction Matters#
Understanding this distinction helps explain:
Testing discrepancies: APIs work in Postman but fail in browsers
Development confusion: Backend developers often don’t encounter CORS issues
Production surprises: CORS errors appear when deploying frontend applications
Tools That Don’t Enforce CORS#
These tools make direct HTTP requests without browser security restrictions:
Postman - API testing tool
curl - Command line HTTP client
HTTPie - Command line HTTP client
Insomnia - API testing tool
Server-side code - Node.js, Python, Java, etc.
Mobile apps - Native iOS/Android applications
Desktop applications - Electron, native desktop apps
Browser Security Context#
Browsers enforce CORS because they’re a shared resource environment:
// In your browser, you might have these tabs open simultaneously:
// Tab 1: https://yourbank.com (logged in)
// Tab 2: https://facebook.com (logged in)
// Tab 3: https://malicioussite.com (unknown trustworthiness)
// Without CORS, tab 3 could access data from tabs 1 and 2!
This is why browsers need CORS - to isolate origins and prevent data leakage between different websites.
Let’s address the most frequent CORS problems developers encounter and provide comprehensive solutions.
Issue 1: Missing Access-Control-Allow-Origin Header#
Error Message:
Access to fetch at 'http://localhost:8000/api/data' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.
Diagnosis: The server isn’t sending any CORS headers.
Solution: Add the CORS header to your server responses:
// Express.js
app.get("/api/data", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
res.json({ data: "your data here" });
});
// Or using middleware
const cors = require("cors");
app.use(
cors({
origin: "http://localhost:3000",
})
);
Issue 2: Origin Mismatch#
Error Message:
Access to fetch at 'http://localhost:8000/api/data' from origin 'http://localhost:3001'
has been blocked by CORS policy: The request client is not a secure context and the
resource's CORS header 'Access-Control-Allow-Origin' is 'http://localhost:3000'.
Diagnosis: Your frontend is running on a different port than what’s configured in CORS headers.
Solution: Update the CORS configuration to match your frontend’s actual origin:
// Check your frontend's actual URL and update accordingly
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3001");
// Or allow multiple origins
const allowedOrigins = [
"http://localhost:3000",
"http://localhost:3001",
"https://yourapp.com",
];
app.use(
cors({
origin: function (origin, callback) {
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
return callback(null, true);
} else {
return callback(new Error("Not allowed by CORS"));
}
},
})
);
Issue 3: Preflight Request Failures#
Error Message:
Access to fetch at 'http://localhost:8000/api/users' from origin 'http://localhost:3000'
has been blocked by CORS policy: Response to preflight request doesn't pass access
control check: No 'Access-Control-Allow-Origin' header is present.
Diagnosis: Your server doesn’t handle OPTIONS requests properly for complex HTTP methods.
Solution: Add proper preflight handling:
// Handle preflight requests
app.options("*", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PUT, PATCH, DELETE, OPTIONS"
);
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, X-Requested-With"
);
res.sendStatus(200);
});
// Or use CORS middleware with preflight support
app.use(
cors({
origin: "http://localhost:3000",
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
})
);
Issue 4: Credentials with Wildcard#
Error Message:
Access to fetch at 'http://localhost:8000/api/profile' from origin 'http://localhost:3000'
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'.
Diagnosis:
You’re using credentials: 'include' but the server has Access-Control-Allow-Origin: *.
Solution: Specify the exact origin instead of using wildcard:
// ❌ This fails with credentials
res.setHeader("Access-Control-Allow-Origin", "*");
// ✅ This works with credentials
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
res.setHeader("Access-Control-Allow-Credentials", "true");
// Using CORS middleware
app.use(
cors({
origin: "http://localhost:3000",
credentials: true,
})
);
Issue 5: Custom Headers Not Allowed#
Error Message:
Access to fetch at 'http://localhost:8000/api/data' from origin 'http://localhost:3000'
has been blocked by CORS policy: Request header field x-api-key is not allowed by
Access-Control-Allow-Headers in preflight response.
Diagnosis: You’re sending custom headers that aren’t explicitly allowed.
Solution: Add custom headers to the allowed headers list:
app.options("*", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, X-API-Key, X-Custom-Header"
);
res.sendStatus(200);
});
// Client-side request
fetch("http://localhost:8000/api/data", {
headers: {
"X-API-Key": "your-api-key",
"Content-Type": "application/json",
},
});
Issue 6: Development vs Production CORS Issues#
Problem: CORS works in development but fails in production.
Diagnosis: Different origins between development and production environments.
Solution: Environment-specific CORS configuration:
const allowedOrigins = {
development: [
"http://localhost:3000",
"http://localhost:3001",
"http://127.0.0.1:3000",
],
production: ["https://yourapp.com", "https://www.yourapp.com"],
};
const environment = process.env.NODE_ENV || "development";
app.use(
cors({
origin: allowedOrigins[environment],
credentials: true,
})
);
Issue 7: Browser Caching Old CORS Headers#
Problem: CORS changes don’t take effect immediately.
Diagnosis: Browser is caching preflight responses.
Solution: Clear browser cache or use cache-busting techniques:
// Disable preflight caching during development
app.options("*", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
res.setHeader("Access-Control-Max-Age", "0"); // Don't cache preflight
res.sendStatus(200);
});
// Or clear browser cache:
// Chrome: DevTools -> Application -> Storage -> Clear storage
// Firefox: DevTools -> Storage -> Clear All
Implementing CORS correctly requires balancing functionality with security. Here are comprehensive best practices to follow.
Security-First Approach#
1. Principle of Least Privilege#
Only allow the minimum necessary origins, methods, and headers:
// ❌ Too permissive
app.use(
cors({
origin: "*",
methods: "*",
allowedHeaders: "*",
})
);
// ✅ Specific and secure
app.use(
cors({
origin: ["https://yourapp.com", "https://admin.yourapp.com"],
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
})
);
2. Never Use Wildcards with Credentials#
This is a critical security rule:
// ❌ DANGEROUS - Never do this
app.use(
cors({
origin: "*",
credentials: true, // This combination is forbidden
})
);
// ✅ Safe approach
app.use(
cors({
origin: function (origin, callback) {
const allowedOrigins = ["https://yourapp.com"];
if (allowedOrigins.includes(origin) || !origin) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
},
credentials: true,
})
);
3. Validate Origins Dynamically#
For applications with dynamic subdomains or multiple environments:
const isValidOrigin = (origin) => {
// Allow localhost for development
if (process.env.NODE_ENV === "development") {
return /^http:\/\/localhost:\d+$/.test(origin);
}
// Allow specific patterns for production
const allowedPatterns = [
/^https:\/\/[\w-]+\.yourapp\.com$/, // Subdomains
/^https:\/\/yourapp\.com$/, // Main domain
];
return allowedPatterns.some((pattern) => pattern.test(origin));
};
app.use(
cors({
origin: function (origin, callback) {
if (!origin) return callback(null, true); // Allow non-browser requests
if (isValidOrigin(origin)) {
callback(null, true);
} else {
callback(new Error(`Origin ${origin} not allowed by CORS`));
}
},
credentials: true,
})
);
Environment-Specific Configuration#
Development Configuration#
// cors-config.js
const developmentConfig = {
origin: [
"http://localhost:3000",
"http://localhost:3001",
"http://127.0.0.1:3000",
"http://127.0.0.1:3001",
],
credentials: true,
optionsSuccessStatus: 200,
// Don't cache preflight responses in development
maxAge: 0,
};
Production Configuration#
const productionConfig = {
origin: [
"https://yourapp.com",
"https://www.yourapp.com",
"https://admin.yourapp.com",
],
credentials: true,
optionsSuccessStatus: 200,
// Cache preflight responses for performance
maxAge: 86400, // 24 hours
};
Configuration Selection#
const corsConfig =
process.env.NODE_ENV === "production" ? productionConfig : developmentConfig;
app.use(cors(corsConfig));
Performance Optimization#
1. Preflight Caching#
// Cache preflight responses to reduce OPTIONS requests
app.use(
cors({
origin: "https://yourapp.com",
maxAge: 86400, // 24 hours
preflightContinue: false,
optionsSuccessStatus: 200,
})
);
2. Route-Specific CORS#
Instead of applying CORS globally, apply it only where needed:
// Public endpoints - allow all origins
app.get("/api/public/*", cors({ origin: "*" }), publicRoutes);
// Protected endpoints - restrict origins
app.use(
"/api/protected/*",
cors({
origin: "https://yourapp.com",
credentials: true,
}),
authMiddleware,
protectedRoutes
);
Monitoring and Logging#
1. CORS Error Logging#
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = ["https://yourapp.com"];
if (allowedOrigins.includes(origin) || !origin) {
callback(null, true);
} else {
// Log blocked requests for monitoring
console.warn(`CORS blocked request from origin: ${origin}`, {
timestamp: new Date().toISOString(),
userAgent: req.get("User-Agent"),
ip: req.ip,
});
callback(new Error("Not allowed by CORS"));
}
},
};
2. Request Analytics#
app.use((req, res, next) => {
const origin = req.get("Origin");
// Log cross-origin requests for analytics
if (origin && origin !== req.get("Host")) {
console.log(`Cross-origin request: ${origin} -> ${req.get("Host")}`, {
method: req.method,
path: req.path,
timestamp: new Date().toISOString(),
});
}
next();
});
Common Middleware Patterns#
1. Express.js with CORS Middleware#
const express = require("express");
const cors = require("cors");
const app = express();
// Configure CORS
const corsOptions = {
origin: function (origin, callback) {
// Allow requests with no origin (mobile apps, Postman, etc.)
if (!origin) return callback(null, true);
const allowedOrigins = ["https://yourapp.com", "https://admin.yourapp.com"];
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
},
credentials: true,
optionsSuccessStatus: 200, // Some legacy browsers choke on 204
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
allowedHeaders: [
"Origin",
"X-Requested-With",
"Content-Type",
"Accept",
"Authorization",
],
};
app.use(cors(corsOptions));
2. Manual CORS Implementation#
const allowedOrigins = ["https://yourapp.com"];
app.use((req, res, next) => {
const origin = req.headers.origin;
// Check if origin is allowed
if (allowedOrigins.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
}
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, PATCH, OPTIONS"
);
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, X-Requested-With"
);
// Handle preflight requests
if (req.method === "OPTIONS") {
res.setHeader("Access-Control-Max-Age", "86400");
return res.sendStatus(200);
}
next();
});
Testing CORS Configuration#
1. Automated Testing#
// test/cors.test.js
const request = require("supertest");
const app = require("../server");
describe("CORS Configuration", () => {
test("should allow requests from allowed origin", async () => {
const response = await request(app)
.get("/api/data")
.set("Origin", "https://yourapp.com");
expect(response.headers["access-control-allow-origin"]).toBe(
"https://yourapp.com"
);
expect(response.status).toBe(200);
});
test("should reject requests from disallowed origin", async () => {
const response = await request(app)
.get("/api/data")
.set("Origin", "https://malicious.com");
expect(response.headers["access-control-allow-origin"]).toBeUndefined();
});
test("should handle preflight requests", async () => {
const response = await request(app)
.options("/api/data")
.set("Origin", "https://yourapp.com")
.set("Access-Control-Request-Method", "POST");
expect(response.status).toBe(200);
expect(response.headers["access-control-allow-methods"]).toContain("POST");
});
});
2. Browser Testing Checklist#
// Create a test page to verify CORS in browser
const testCORS = async () => {
const tests = [
// Test 1: Simple GET request
{
name: "Simple GET",
request: () => fetch("/api/data"),
},
// Test 2: POST with JSON
{
name: "POST with JSON",
request: () =>
fetch("/api/data", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ test: true }),
}),
},
// Test 3: Request with credentials
{
name: "Credentialed request",
request: () =>
fetch("/api/protected", {
credentials: "include",
}),
},
// Test 4: Custom headers
{
name: "Custom headers",
request: () =>
fetch("/api/data", {
headers: { "X-Custom-Header": "test" },
}),
},
];
for (const test of tests) {
try {
const response = await test.request();
console.log(`✅ ${test.name}: ${response.status}`);
} catch (error) {
console.log(`❌ ${test.name}: ${error.message}`);
}
}
};
Throughout this comprehensive guide, we’ve explored CORS from every angle - from understanding the fundamental security problems it solves to implementing robust, production-ready solutions. Let’s recap the key insights:
The Security Foundation#
CORS exists because browsers are shared resources where users access multiple websites simultaneously. Without CORS, any website could access your data from other sites, leading to catastrophic security breaches. The same-origin policy provides the default protection, while CORS offers a controlled way to relax these restrictions.
Key Technical Insights#
Origins are defined by scheme + host + port - understanding this tuple is crucial for CORS configuration
Servers control access - not clients. CORS errors are always server-side configuration issues
Credentials require explicit origins - wildcards never work with authenticated requests
Complex requests trigger preflight - be prepared to handle OPTIONS requests
Browser-only enforcement - CORS doesn’t apply to server-to-server communication
Implementation Best Practices#
Start restrictive, then relax as needed - follow the principle of least privilege
Use environment-specific configurations - development and production have different needs
Monitor and log CORS requests - understand who’s accessing your APIs
Test thoroughly - automated testing prevents production CORS surprises
Cache preflight responses - optimize performance while maintaining security
Beyond Basic Implementation#
Modern CORS implementation goes beyond just “making it work” - it requires thinking about:
Security implications of each configuration decision
Performance optimization through smart caching strategies
Monitoring and observability to understand access patterns
Scalability considerations for applications with dynamic origins
The Bigger Picture#
CORS is more than just a technical hurdle - it’s a fundamental web security mechanism that enables the modern web’s flexibility while protecting users’ privacy and security. By understanding CORS deeply, you’re not just solving immediate technical problems, but contributing to a more secure web ecosystem.
Remember, every time you see a CORS error, you’re witnessing a security feature working correctly to protect users. With the knowledge from this guide, you now have the tools to configure CORS properly, balancing accessibility with security.
The web’s evolution continues, and CORS remains a cornerstone of browser security. Master it well, and you’ll build applications that are both functional and secure - exactly what the modern web demands.
Discussion
0Leave a comment