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:
This could be:
This is your backend that:
The interaction between these components follows a simple request-response cycle:
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.
Picture this realistic scenario:
https://yoursite.dev
facebook.com
)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?
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",
}),
});
The implications become even more severe with financial services:
hdfc.com
) in one tab// 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",
}),
});
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 to facebook.com
(Origin B)malicioussite.com
(Origin A) making requests to hdfc.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.
From a browser's perspective, an origin is defined by a tuple (combination) of three components:
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/home
https://yoursite.dev:443/api/users
https://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)
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.
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.dev
can only communicate with servers on yoursite.dev
facebook.com
hdfc.com
api.yoursite.dev
(different subdomain)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");
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
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.
When you make a cross-origin request, here's exactly what happens:
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
Your server receives the request and sees:
https://yoursite.dev
https://api.yoursite.dev
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.
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 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/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/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;
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.
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.
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
/data
route has CORS enabledThe wildcard (*
) in CORS headers is both powerful and dangerous. Let's explore when to use it and when to avoid it.
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:
The wildcard is suitable for:
// 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",
});
});
// Serving static assets
app.get("/assets/*", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
// Serve CSS, JS, images, etc.
});
// 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
});
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:
When working with authentication (cookies, authorization headers, etc.), CORS has additional restrictions that provide an extra layer of security.
Credentials include:
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));
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" });
});
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:
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 Requests (sent directly):
GET
, HEAD
, POST
Accept
, Content-Type
(with restrictions)application/x-www-form-urlencoded
, multipart/form-data
, or text/plain
Complex Requests (require preflight):
PUT
, PATCH
, DELETE
, OPTIONS
, etc.Authorization
, X-Custom-Header
, etc.application/json
, application/xml
, etc.When you make a complex request, the browser follows this two-step process:
// 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
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);
});
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
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 requests add network overhead. You can optimize them by:
res.setHeader("Access-Control-Max-Age", "86400"); // Cache for 24 hours
// 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 restrictions are only enforced by browsers. Here's why:
# This works perfectly - no CORS restrictions
curl -X GET "http://localhost:8000/data" \
-H "Content-Type: application/json"
// 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
// 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);
}
}
Understanding this distinction helps explain:
These tools make direct HTTP requests without browser security restrictions:
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.
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",
})
);
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"));
}
},
})
);
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"],
})
);
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,
})
);
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",
},
});
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,
})
);
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.
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,
})
);
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,
})
);
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,
})
);
// 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,
};
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
};
const corsConfig =
process.env.NODE_ENV === "production" ? productionConfig : developmentConfig;
app.use(cors(corsConfig));
// Cache preflight responses to reduce OPTIONS requests
app.use(
cors({
origin: "https://yourapp.com",
maxAge: 86400, // 24 hours
preflightContinue: false,
optionsSuccessStatus: 200,
})
);
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
);
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"));
}
},
};
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();
});
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));
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();
});
// 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");
});
});
// 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:
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.
Modern CORS implementation goes beyond just "making it work" - it requires thinking about:
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.