CORS & Security Headers
Checking access...
HTTP headers are your API’s first line of defense. CORS controls which origins can access your resources, and security headers like Content-Security-Policy and HSTS protect against XSS, clickjacking, and protocol downgrade attacks.
What is CORS?
Cross-Origin Resource Sharing (CORS) is a browser security mechanism that controls which websites can access resources from your server. Without CORS, a malicious site could make authenticated requests to your API through a user’s browser.
Same-Origin Policy
Browsers enforce a same-origin policy: a page at https://app.example.com can only make requests to https://app.example.com by default. Different port, protocol, or host = different origin.
Origin A: https://app.example.comOrigin B: https://api.example.com ← Different host → BLOCKEDOrigin C: https://app.example.com:8080 ← Different port → BLOCKEDOrigin D: http://app.example.com ← Different protocol → BLOCKEDOrigin E: https://app.example.com ← Same origin → ALLOWEDHow CORS Works
The browser sends a preflight OPTIONS request before certain cross-origin requests, then checks the server’s response headers:
# Preflight request (browser sends automatically)OPTIONS /api/usersOrigin: https://myapp.comAccess-Control-Request-Method: POSTAccess-Control-Request-Headers: Content-Type, Authorization
# Server responseHTTP/1.1 204 No ContentAccess-Control-Allow-Origin: https://myapp.comAccess-Control-Allow-Methods: GET, POST, PUT, DELETEAccess-Control-Allow-Headers: Content-Type, AuthorizationAccess-Control-Max-Age: 86400Configuring CORS in Express
Basic Setup with cors Package
npm install corsconst express = require("express");const cors = require("cors");
const app = express();
// Allow all origins (development only!)app.use(cors());Production Configuration
const corsOptions = { origin: "https://myapp.com", // Single allowed origin methods: ["GET", "POST", "PUT", "DELETE"], // Allowed HTTP methods allowedHeaders: ["Content-Type", "Authorization"], // Allowed request headers exposedHeaders: ["X-Request-Id"], // Headers exposed to the browser credentials: true, // Allow cookies/auth headers maxAge: 86400, // Cache preflight for 24 hours};
app.use(cors(corsOptions));Multiple Origins
const allowedOrigins = [ "https://myapp.com", "https://admin.myapp.com", "http://localhost:5173", // Dev frontend];
app.use(cors({ origin: (origin, callback) => { // Allow requests with no origin (server-to-server, Postman, curl) if (!origin || allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error("Not allowed by CORS")); } }, credentials: true,}));CORS Error Handling
// Global CORS error handlerapp.use((err, req, res, next) => { if (err.message === "Not allowed by CORS") { return res.status(403).json({ error: "CORS_ERROR", message: "Origin not allowed", }); } next(err);});Security Headers with Helmet
Helmet sets various HTTP security headers. It’s a collection of smaller middleware functions.
npm install helmetconst helmet = require("helmet");
// Default (recommended for most apps)app.use(helmet());Headers Set by Helmet (Default)
# Default helmet response headersContent-Security-Policy: default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requestsCross-Origin-Embedder-Policy: require-corpCross-Origin-Opener-Policy: same-originCross-Origin-Resource-Policy: same-originExpect-CT: max-age=0Origin-Agent-Cluster: ?1Referrer-Policy: no-referrerStrict-Transport-Security: max-age=15552000; includeSubDomainsX-Content-Type-Options: nosniffX-DNS-Prefetch-Control: offX-Download-Options: noopenX-Frame-Options: SAMEORIGINX-Permitted-Cross-Domain-Policies: noneX-XSS-Protection: 0Customizing Helmet
app.use( helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "https://cdn.example.com"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https://images.example.com"], connectSrc: ["'self'", "https://api.example.com"], fontSrc: ["'self'", "https://fonts.gstatic.com"], objectSrc: ["'none'"], upgradeInsecureRequests: [], }, }, hsts: { maxAge: 31536000, // 1 year includeSubDomains: true, preload: true, }, frameguard: { action: "deny", // Block all framing (DENY vs SAMEORIGIN) }, }));Key Security Headers Explained
Content-Security-Policy (CSP)
CSP prevents XSS attacks by specifying which sources are allowed to load resources:
Content-Security-Policy: default-src 'self'; script-src 'self' https://apis.google.comCommon directives:
| Directive | Controls | Example |
|---|---|---|
default-src | Fallback for all resource types | default-src 'self' |
script-src | JavaScript sources | script-src 'self' 'nonce-abc123' |
style-src | Stylesheet sources | style-src 'self' 'unsafe-inline' |
img-src | Image sources | img-src 'self' data: https: |
connect-src | API/fetch/XHR targets | connect-src 'self' https://api.example.com |
frame-ancestors | Who can embed the page in iframe | frame-ancestors 'none' |
form-action | Where forms can submit | form-action 'self' |
Strict-Transport-Security (HSTS)
Forces HTTPS connections and prevents SSL stripping:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadmax-age: Seconds to enforce HTTPSincludeSubDomains: Apply to all subdomainspreload: Allow inclusion in browser HSTS preload lists
X-Frame-Options
Prevents clickjacking by controlling iframe embedding:
X-Frame-Options: DENY # No iframe embeddingX-Frame-Options: SAMEORIGIN # Only same-origin iframesX-Content-Type-Options
Prevents MIME-type sniffing (browsers guessing content type):
X-Content-Type-Options: nosniffCORS for Different Environments
Development
NODE_ENV=developmentFRONTEND_URL=http://localhost:5173
// cors.config.jsfunction getCorsOptions() { if (process.env.NODE_ENV === "development") { return { origin: process.env.FRONTEND_URL || "http://localhost:5173", credentials: true, }; }
return { origin: process.env.FRONTEND_URL, credentials: true, maxAge: 86400, };}Microservices
// API Gateway CORS configurationconst gatewayCors = { origin: [ "https://app.example.com", "https://admin.example.com", ], methods: ["GET", "POST", "PUT", "DELETE", "PATCH"], allowedHeaders: ["Content-Type", "Authorization", "X-Request-Id"], exposedHeaders: ["X-Request-Id", "X-RateLimit-Remaining"], credentials: true, maxAge: 86400,};Common CORS Issues & Fixes
”No ‘Access-Control-Allow-Origin’ header”
Problem: Server doesn’t include the CORS header.
Fix: Configure the server to send the header:
// Manual CORS header (if not using cors package)app.use((req, res, next) => { res.header("Access-Control-Allow-Origin", "https://myapp.com"); res.header("Access-Control-Allow-Headers", "Content-Type, Authorization"); res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"); if (req.method === "OPTIONS") { return res.sendStatus(204); } next();});CORS Error with Credentials
Problem: Using credentials: true but Access-Control-Allow-Origin is *.
Fix: Wildcard origin doesn’t work with credentials. Specify exact origin:
// ❌ Will not workapp.use(cors({ origin: "*", credentials: true }));
// ✅ Must be specificapp.use(cors({ origin: "https://myapp.com", credentials: true }));Preflight Caching
Reduce preflight requests by caching the preflight response:
app.use(cors({ maxAge: 86400, // 24 hours — browser won't preflight again}));Security Headers Checklist
// Complete security headers setupconst helmet = require("helmet");const cors = require("cors");const rateLimit = require("express-rate-limit");
const app = express();
// 1. Security headersapp.use(helmet());
// 2. CORSapp.use(cors({ origin: process.env.FRONTEND_URL, credentials: true, maxAge: 86400,}));
// 3. Rate limitingapp.use("/api/", rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100,}));
// 4. Cookie securityapp.use(require("cookie-parser")());app.use((req, res, next) => { res.cookie("session", req.sessionID, { httpOnly: true, // No JavaScript access secure: true, // HTTPS only sameSite: "strict", // Same-site requests only maxAge: 7 * 24 * 60 * 60 * 1000, }); next();});Quick Reference
// Minimum CORS setupconst cors = require("cors");app.use(cors({ origin: "https://myapp.com", credentials: true }));
// Minimum helmet setupconst helmet = require("helmet");app.use(helmet());
// Commonly needed CSP for SPA with CDN assetshelmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'", "https://api.example.com"], }, },});Practice Exercises
CORS configuration: Create an Express server with two endpoints. Configure CORS so only
http://localhost:5173can access POST endpoints while GET is open to all origins.Helmet customization: Build an Express app where the CSP allows loading scripts from
cdn.example.comand images fromimages.example.com. Verify with response headers.Multiple origins: Write a CORS middleware that accepts requests from
https://app.com,https://admin.app.com, and any localhost origin. Reject everything else with a clear error message.