|$ curl https://forge-ai.dev/api/markdown?path=docs/security/cors
$cat docs/security-—-cors-&-csrf.md
updated Recently·14 min read·published

Security — CORS & CSRF

SecurityIntermediate
Introduction

CORS (Cross-Origin Resource Sharing) and CSRF (Cross-Site Request Forgery) are two distinct but related security concerns in web applications. CORS controls which external origins can access your resources, while CSRF protects against unauthorized commands from authenticated users.

Misconfiguring either can leave your application vulnerable to data theft, unauthorized actions, or cross-origin attacks. This guide covers the fundamentals and practical configuration for both.

Same-Origin Policy

The Same-Origin Policy (SOP) is the browser's fundamental security mechanism. It restricts how a document or script loaded from one origin can interact with resources from another origin. An origin is defined by the scheme (protocol), host (domain), and port.

URLSame Origin?Reason
https://example.com/page1YesSame scheme, host, port
https://api.example.comNoDifferent host (subdomain)
http://example.comNoDifferent scheme
https://example.com:8080NoDifferent port

info

The Same-Origin Policy does not block all cross-origin requests — it blocks reading the response. Simple write operations like form submissions and link navigation are allowed. The policy primarily prevents malicious sites from reading data from other origins.
CORS Headers

CORS uses HTTP headers to relax the Same-Origin Policy. The server specifies which origins, methods, and headers are allowed. The browser enforces these rules.

cors-headers.txt
JavaScript
1// CORS response headers explained
2// Access-Control-Allow-Origin: Which origins are allowed
3Access-Control-Allow-Origin: https://example.com
4// Access-Control-Allow-Origin: * (allow any origin — use with caution)
5
6// Access-Control-Allow-Methods: Allowed HTTP methods
7Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
8
9// Access-Control-Allow-Headers: Allowed request headers
10Access-Control-Allow-Headers: Content-Type, Authorization, X-CSRF-Token
11
12// Access-Control-Expose-Headers: Which response headers the client can read
13Access-Control-Expose-Headers: X-RateLimit-Remaining, X-Request-Id
14
15// Access-Control-Allow-Credentials: Allow cookies/auth headers
16Access-Control-Allow-Credentials: true
17
18// Access-Control-Max-Age: How long to cache the preflight response (seconds)
19Access-Control-Max-Age: 86400

danger

Never use Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true. This combination is invalid and browsers will reject the response. If you need credentials, you must specify explicit origins.
Preflight Requests

For requests that are not "simple" (e.g., custom headers, non-standard content types, or methods other than GET/POST), the browser sends a preflight OPTIONS request before the actual request. The server must respond correctly for the browser to proceed.

preflight-options.txt
JavaScript
1// Preflight OPTIONS request (automatically sent by browser)
2OPTIONS /api/data HTTP/1.1
3Origin: https://app.example.com
4Access-Control-Request-Method: POST
5Access-Control-Request-Headers: Content-Type, Authorization
6
7// Server response to preflight
8HTTP/1.1 204 No Content
9Access-Control-Allow-Origin: https://app.example.com
10Access-Control-Allow-Methods: GET, POST, PUT, DELETE
11Access-Control-Allow-Headers: Content-Type, Authorization
12Access-Control-Max-Age: 86400
13
14// Express middleware handling preflight globally
15app.options('*', cors()); // Enable preflight for all routes
16
17// Or handle manually
18app.use((req, res, next) => {
19 res.header('Access-Control-Allow-Origin', 'https://app.example.com');
20 res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
21 res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
22 if (req.method === 'OPTIONS') {
23 return res.sendStatus(204);
24 }
25 next();
26});
CORS Configuration in Express

The cors package in Express makes it straightforward to configure CORS. Use environment-based configuration to allow different origins in development vs production.

cors-express.js
JavaScript
1const cors = require('cors');
2const express = require('express');
3const app = express();
4
5// Development — allow all origins
6if (process.env.NODE_ENV === 'development') {
7 app.use(cors({ origin: true, credentials: true }));
8}
9
10// Production — restrict to specific origins
11const allowedOrigins = [
12 'https://example.com',
13 'https://www.example.com',
14 'https://admin.example.com',
15];
16
17app.use(cors({
18 origin: (origin, callback) => {
19 // Allow requests with no origin (server-to-server, curl, etc.)
20 if (!origin) return callback(null, true);
21 if (allowedOrigins.includes(origin)) {
22 callback(null, true);
23 } else {
24 callback(new Error('Not allowed by CORS'));
25 }
26 },
27 methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
28 allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token'],
29 credentials: true,
30 maxAge: 86400,
31}));
32
33// Per-route CORS
34const corsOptions = {
35 origin: 'https://trusted-third-party.com',
36 methods: ['GET'],
37};
38
39app.get('/api/public', cors(corsOptions), (req, res) => {
40 res.json({ data: 'Public data' });
41});

best practice

Use a function-based origin callback when you have dynamic allowed origins (e.g., from a database or environment variable list). Avoid string-only configuration in production if you need multiple origins. Always validate origins server-side.
CORS in Next.js API Routes

In Next.js, CORS headers must be set manually in API route handlers or via middleware. The App Router provides access to the full Response API for setting headers.

cors-nextjs.ts
JavaScript
1// App Router — API route with CORS (app/api/data/route.ts)
2import { NextRequest, NextResponse } from 'next/server';
3
4const allowedOrigins = ['https://example.com', 'https://app.example.com'];
5
6function getCorsHeaders(origin: string | null) {
7 const isAllowed = origin && allowedOrigins.includes(origin);
8 return {
9 'Access-Control-Allow-Origin': isAllowed ? origin : '',
10 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
11 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
12 'Access-Control-Allow-Credentials': 'true',
13 'Access-Control-Max-Age': '86400',
14 };
15}
16
17export async function OPTIONS(request: NextRequest) {
18 const origin = request.headers.get('origin');
19 return NextResponse.json(
20 {},
21 { status: 204, headers: getCorsHeaders(origin) }
22 );
23}
24
25export async function GET(request: NextRequest) {
26 const origin = request.headers.get('origin');
27 const data = await fetchData();
28 return NextResponse.json(data, {
29 headers: getCorsHeaders(origin),
30 });
31}
32
33// Pages Router — API route with CORS
34export default function handler(req, res) {
35 const origin = req.headers.origin;
36 const allowedOrigin = allowedOrigins.includes(origin) ? origin : '';
37
38 res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
39 res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
40 res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
41 res.setHeader('Access-Control-Allow-Credentials', 'true');
42
43 if (req.method === 'OPTIONS') {
44 res.status(200).end();
45 return;
46 }
47
48 // Handle the request
49 res.status(200).json({ message: 'Success' });
50}
CORS in nginx

nginx can add CORS headers at the reverse proxy layer, which is useful when your application server doesn't handle CORS natively or you want a centralized configuration.

cors-nginx.conf
TEXT
1# nginx CORS configuration
2server {
3 listen 443 ssl;
4 server_name api.example.com;
5
6 # Handle preflight requests
7 if ($request_method = 'OPTIONS') {
8 add_header Access-Control-Allow-Origin $http_origin;
9 add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';
10 add_header Access-Control-Allow-Headers 'Content-Type, Authorization, X-CSRF-Token';
11 add_header Access-Control-Allow-Credentials 'true';
12 add_header Access-Control-Max-Age 86400;
13 add_header Content-Length 0;
14 add_header Content-Type text/plain;
15 return 204;
16 }
17
18 # Handle actual requests
19 location /api/ {
20 add_header Access-Control-Allow-Origin $http_origin;
21 add_header Access-Control-Allow-Credentials 'true';
22
23 if ($http_origin !~ '^https://(example\.com|app\.example\.com)$') {
24 add_header Access-Control-Allow-Origin ''; # Block unauthorized origins
25 }
26
27 proxy_pass http://backend:3000;
28 proxy_set_header Host $host;
29 proxy_set_header X-Real-IP $remote_addr;
30 }
31}
32
33# CORS with wildcard (public API — no credentials)
34server {
35 listen 443 ssl;
36 server_name api.public.example.com;
37
38 location / {
39 add_header Access-Control-Allow-Origin '*';
40 add_header Access-Control-Allow-Methods 'GET';
41 # Do NOT set Allow-Credentials with wildcard
42 proxy_pass http://backend:3000;
43 }
44}
CORS in Cloud Functions

Serverless platforms like Cloudflare Workers, AWS Lambda, and Vercel Edge Functions require explicit CORS handling since they act as both the application and the edge.

cors-worker.js
JavaScript
1// Cloudflare Worker CORS handler
2addEventListener('fetch', event => {
3 event.respondWith(handleRequest(event.request));
4});
5
6async function handleRequest(request) {
7 const origin = request.headers.get('Origin');
8 const allowedOrigins = ['https://example.com', 'https://app.example.com'];
9 const corsOrigin = allowedOrigins.includes(origin) ? origin : '';
10
11 // Handle preflight
12 if (request.method === 'OPTIONS') {
13 return new Response(null, {
14 headers: {
15 'Access-Control-Allow-Origin': corsOrigin,
16 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
17 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
18 'Access-Control-Max-Age': '86400',
19 },
20 });
21 }
22
23 const response = await fetch(request);
24 const corsResponse = new Response(response.body, response);
25 corsResponse.headers.set('Access-Control-Allow-Origin', corsOrigin);
26 corsResponse.headers.set('Access-Control-Allow-Credentials', 'true');
27
28 return corsResponse;
29}
30
31// AWS Lambda + API Gateway (serverless.yml)
32// Add CORS in the API Gateway configuration:
33// Api:
34// Cors:
35// AllowOrigin: "'https://example.com'"
36// AllowHeaders: "'Content-Type,Authorization'"
37// AllowMethods: "'GET,POST,PUT,DELETE,OPTIONS'"
38// AllowCredentials: "'true'"
CSRF (Cross-Site Request Forgery)

CSRF attacks trick an authenticated user into submitting an unwanted request. The attack succeeds because the browser automatically includes cookies (including session cookies) with every request to the target domain. CSRF protection ensures that requests originate from the actual application, not an external site.

warning

CORS does not prevent CSRF. CORS controls cross-origin reading of responses, while CSRF exploits the automatic inclusion of credentials in cross-origin write requests. Even if CORS is properly configured, CSRF protection is still necessary for state-changing requests.
CSRF Tokens

CSRF tokens are unique, unpredictable values generated by the server and included in forms or API requests. The server validates the token before processing state-changing requests.

csrf-tokens.js
JavaScript
1// Server-side CSRF token generation and validation (Express)
2const crypto = require('crypto');
3const csrf = require('csurf');
4const express = require('express');
5const app = express();
6
7// Using csurf middleware (Express 4)
8const csrfProtection = csrf({ cookie: true });
9app.use(csrfProtection);
10
11// Generate token and pass to template
12app.get('/form', (req, res) => {
13 res.render('form', { csrfToken: req.csrfToken() });
14});
15
16// The middleware automatically validates CSRF on POST, PUT, PATCH, DELETE
17app.post('/transfer', (req, res) => {
18 // If CSRF token is invalid, middleware throws a ForbiddenError
19 res.send('Transfer completed');
20});
21
22// Custom CSRF implementation
23const tokens = new Map();
24
25function generateToken(sessionId) {
26 const token = crypto.randomBytes(32).toString('hex');
27 tokens.set(token, { sessionId, expires: Date.now() + 3600000 });
28 return token;
29}
30
31function validateToken(token, sessionId) {
32 const stored = tokens.get(token);
33 if (!stored) return false;
34 if (stored.sessionId !== sessionId) return false;
35 if (Date.now() > stored.expires) {
36 tokens.delete(token);
37 return false;
38 }
39 tokens.delete(token); // Single-use token
40 return true;
41}
42
43// Frontend — include CSRF token in every state-changing request
44// <form action="/transfer" method="POST">
45// <input type="hidden" name="_csrf" value="<%= csrfToken %>">
46// <input type="text" name="amount">
47// <button type="submit">Transfer</button>
48// </form>
49
50// For API requests, include token in header
51fetch('/api/transfer', {
52 method: 'POST',
53 headers: {
54 'Content-Type': 'application/json',
55 'X-CSRF-Token': csrfToken,
56 },
57 body: JSON.stringify({ amount: 100 }),
58});
SameSite Cookies

The SameSite cookie attribute lets the browser decide whether to include cookies with cross-origin requests. It is one of the most effective CSRF defenses.

SameSite ValueBehaviorUse Case
StrictCookies not sent for any cross-site requestBanking, sensitive actions
LaxCookies sent for top-level navigations (GET)Default, e-commerce, content sites
NoneCookies sent for all cross-site requestsThird-party widgets, must use Secure flag
samesite-cookies.js
JavaScript
1// Setting SameSite cookies in Express
2const express = require('express');
3const session = require('express-session');
4const app = express();
5
6// Session cookie with SameSite=Lax (default in modern browsers)
7app.use(session({
8 secret: process.env.SESSION_SECRET,
9 resave: false,
10 saveUninitialized: false,
11 cookie: {
12 httpOnly: true,
13 secure: process.env.NODE_ENV === 'production',
14 sameSite: 'lax', // Lax — safe default
15 maxAge: 24 * 60 * 60 * 1000, // 24 hours
16 },
17}));
18
19// Sensitive operations — Strict SameSite
20app.use('/api/admin', session({
21 secret: process.env.ADMIN_SECRET,
22 cookie: {
23 httpOnly: true,
24 secure: true,
25 sameSite: 'strict', // Strict — only same-site requests
26 },
27}));
28
29// Third-party context — None (requires Secure + HTTPS)
30// Only if you need embedded widgets across domains
31app.use(session({
32 cookie: {
33 sameSite: 'none',
34 secure: true, // Required when sameSite is 'none'
35 },
36}));
37
38// Setting SameSite via response header directly
39res.setHeader('Set-Cookie', [
40 `session=${sessionId}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400`,
41 `csrf_token=${token}; HttpOnly; Secure; SameSite=Strict; Path=/api`,
42].join(', '));
CSRF vs CORS

CORS and CSRF are often confused because both involve cross-origin requests. Understanding the distinction is critical for secure configuration.

AspectCORSCSRF
What it protectsReading data cross-originWriting data cross-origin
Enforcement pointBrowserServer
Attack typeInformation disclosureUnauthorized state change
Auth involvedMay or may not include credentialsRequires authentication (session cookie)
Primary defenseServer response headersCSRF tokens, SameSite cookies
Best Practices
  • Set SameSite=Lax as the default for session cookies — it prevents CSRF in most scenarios without breaking user experience.
  • Use SameSite=Strict for sensitive operations like password changes and money transfers.
  • Always use CSRF tokens as a defense-in-depth measure, even with SameSite cookies (older browsers may ignore SameSite).
  • Never use Access-Control-Allow-Origin: * with credentials. Always whitelist specific origins in production.
  • Cache preflight responses with Access-Control-Max-Age to reduce latency, but not longer than 24 hours.
  • Validate the Origin header on the server — do not rely solely on the Referer header, which can be missing or spoofed.
  • Implement CSRF protection as middleware applied globally to all state-changing routes (POST, PUT, PATCH, DELETE).
  • Use custom request headers (X-Requested-By, X-CSRF-Token) as an additional CSRF defense for API requests.

info

Many modern frameworks have CSRF protection built in. Next.js App Router includes CSRF protection for Server Actions. Express has the csurf middleware. Django and Ruby on Rails include CSRF tokens automatically. Use the framework default before implementing custom solutions.
$Blueprint — Engineering Documentation·Section ID: SEC-CORS·Revision: 1.0