CORS Configuration with Claude Code: Origin Control and Preflight Optimization
Source: Dev.to
CORS Configuration Rules
Security (required)
- Never use
Access-Control-Allow-Origin: *in production. - Load allowed origins from environment variables (no hard‑coding).
- When using credentials, only allow specific origins (incompatible with
*). - Only allow HTTP methods that are explicitly needed; e.g.,
DELETEmust be listed explicitly.
Preflight
- Register CORS middleware before all routes.
- Return 204 for
OPTIONSrequests (no response body). - Cache preflight responses for 24 hours (
max‑age=86400).
Headers
- Request headers to allow:
Content-Type,Authorization,X-Request-ID. - Response headers to expose:
X-Total-Count,X-Request-ID.
Generated CORS configuration
src/middleware/cors.ts
// src/middleware/cors.ts
import cors from 'cors';
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(',').map(o => o.trim()) ?? [];
export const corsMiddleware = cors({
origin: (origin, callback) => {
// Same‑origin or server‑to‑server requests have undefined origin
if (!origin) return callback(null, true);
if (ALLOWED_ORIGINS.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`CORS: origin ${origin} not allowed`));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
exposedHeaders: ['X-Total-Count', 'X-Request-ID'],
maxAge: 86400,
});
src/app.ts
// src/app.ts
import express from 'express';
import { corsMiddleware } from './middleware/cors';
const app = express();
// CORS must be registered before all routes
app.use(corsMiddleware);
// Return 204 for OPTIONS preflight
app.options('*', corsMiddleware, (req, res) => {
res.status(204).end();
});
app.use(express.json());
// ...other route handlers
Multi‑tenant CORS
src/middleware/dynamicCors.ts
// src/middleware/dynamicCors.ts
import { Request, Response, NextFunction } from 'express';
import redis from './redis'; // assume a configured Redis client
import prisma from './prisma'; // assume a configured Prisma client
async function getAllowedOrigins(tenantId: string): Promise {
const cacheKey = `cors:tenant:${tenantId}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const tenant = await prisma.tenant.findUnique({
where: { id: tenantId },
select: { allowedOrigins: true },
});
const origins = tenant?.allowedOrigins ?? [];
await redis.set(cacheKey, JSON.stringify(origins), { EX: 300 }); // 5 min TTL
return origins;
}
export const dynamicCorsMiddleware = async (req: Request, res: Response, next: NextFunction) => {
const tenantId = req.headers['x-tenant-id'] as string;
const origin = req.headers.origin as string;
if (!tenantId || !origin) return next();
const allowedOrigins = await getAllowedOrigins(tenantId);
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Vary', 'Origin'); // Prevent CDN/proxy cache pollution
}
next();
};
.env.example
# Comma‑separated list of allowed origins for production
ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com
# Development (uncomment as needed)
# ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
Design notes
- CLAUDE.md: Enforce “no
*in production”, load origins from environment, and note the credentials restriction. - Origin validation: Implemented as a dynamic allow‑list check; no hard‑coded values.
- Vary header: Added to avoid CDN/proxy cache pollution when origins differ per request.
- Preflight cache:
maxAge: 86400reduces unnecessaryOPTIONSround‑trips.
For further reading on CORS security checks, see the Security Pack (¥1,480) which includes /security-check for detecting wildcard origins, credential leaks, and missing Vary headers.
Myouga (@myougatheaxo) – Claude Code engineer focused on API security.