Security Headers
Security headers with Helmet.js, Content Security Policy, CORS configuration, CSRF protection, rate limiting patterns, and Next.js security headers
Security headers are the first line of defense between your application and the browser. They instruct the browser to enforce constraints — which scripts can run, which origins can embed your content, whether cookies travel over plain HTTP. Helmet.js applies sensible defaults for Express-based servers. For Next.js, you configure headers directly in `next.config.js` or middleware. The goal is defense in depth: CSP prevents XSS, CORS controls cross-origin access, CSRF tokens guard state-changing requests, and rate limiting caps abuse. None of these replace proper input validation, but together they dramatically shrink the attack surface. ## Key Points - Start with `Content-Security-Policy-Report-Only` to identify violations before enforcing. Review reports for at least a week before switching to enforcing mode. - Set `Strict-Transport-Security` with `includeSubDomains` and `preload` on production. Submit your domain to the HSTS preload list. - Use `SameSite=Strict` or `SameSite=Lax` on all cookies. Combine with `HttpOnly` and `Secure` flags. - Apply rate limits per route type: strict on auth endpoints, moderate on API endpoints, lenient on static assets. - Use `Permissions-Policy` to explicitly disable browser features you do not use (camera, microphone, geolocation). - For CORS, never use `origin: "*"` with `credentials: true`. Browsers will reject this combination, and it signals a misconfiguration. - Use nonces rather than `'unsafe-inline'` for CSP script-src when possible. This is the strongest XSS mitigation available via headers. - **Setting `unsafe-inline` and `unsafe-eval` in CSP and calling it done.** These directives effectively disable CSP protection against XSS. Use nonces or hashes instead. - **Applying CORS with `origin: "*"` in production.** This allows any website to make requests to your API. Always whitelist specific origins. - **Skipping CSRF protection because "we use JWTs".** If the JWT is stored in a cookie (common for SSR apps), CSRF attacks are still possible. Token-in-header or SameSite cookies are needed. - **Rate limiting only on the application layer behind a load balancer.** Each instance sees a fraction of traffic. Use a shared store (Redis) or an edge rate limiter. - **Setting security headers only on HTML pages.** API responses also need CORS, rate limit headers, and content type protection. Apply headers globally and customize per route. ## Quick Example ```typescript // For Express/Node.js npm install helmet cors csrf-csrf cookie-parser express-rate-limit // For Next.js (no extra deps for headers; optional for CSRF) npm install csrf-csrf cookie-parser ```
skilldb get security-ratelimit-skills/Security HeadersFull skill: 431 linesSecurity Headers and HTTP Protection
Core Philosophy
Security headers are the first line of defense between your application and the browser. They instruct the browser to enforce constraints — which scripts can run, which origins can embed your content, whether cookies travel over plain HTTP. Helmet.js applies sensible defaults for Express-based servers. For Next.js, you configure headers directly in next.config.js or middleware. The goal is defense in depth: CSP prevents XSS, CORS controls cross-origin access, CSRF tokens guard state-changing requests, and rate limiting caps abuse. None of these replace proper input validation, but together they dramatically shrink the attack surface.
Setup
Installation
// For Express/Node.js
npm install helmet cors csrf-csrf cookie-parser express-rate-limit
// For Next.js (no extra deps for headers; optional for CSRF)
npm install csrf-csrf cookie-parser
Express Base Configuration
// server.ts
import express from "express";
import helmet from "helmet";
import cors from "cors";
import cookieParser from "cookie-parser";
import rateLimit from "express-rate-limit";
const app = express();
app.use(helmet());
app.use(cookieParser());
app.use(express.json());
app.listen(3000);
Key Techniques
Helmet.js Default Headers
// Helmet sets these headers by default:
// Content-Security-Policy: default-src 'self'
// Cross-Origin-Opener-Policy: same-origin
// Cross-Origin-Resource-Policy: same-origin
// Origin-Agent-Cluster: ?1
// Referrer-Policy: no-referrer
// Strict-Transport-Security: max-age=15552000; includeSubDomains
// X-Content-Type-Options: nosniff
// X-DNS-Prefetch-Control: off
// X-Download-Options: noopen
// X-Frame-Options: SAMEORIGIN
// X-Permitted-Cross-Domain-Policies: none
// X-XSS-Protection: 0
import helmet from "helmet";
app.use(helmet());
Content Security Policy (CSP)
// Customizing CSP for a typical web app
import helmet from "helmet";
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'strict-dynamic'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https://cdn.example.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
connectSrc: ["'self'", "https://api.example.com"],
frameSrc: ["'none'"],
objectSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
upgradeInsecureRequests: [],
},
},
})
);
CSP with Nonce for Inline Scripts
// middleware for generating per-request nonce
import crypto from "crypto";
import helmet from "helmet";
import { Request, Response, NextFunction } from "express";
app.use((req: Request, res: Response, next: NextFunction) => {
res.locals.cspNonce = crypto.randomBytes(16).toString("base64");
next();
});
app.use((req: Request, res: Response, next: NextFunction) => {
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", `'nonce-${res.locals.cspNonce}'`],
styleSrc: ["'self'", `'nonce-${res.locals.cspNonce}'`],
},
},
})(req, res, next);
});
// In your template/response:
// <script nonce="<%= cspNonce %>">...</script>
CORS Configuration
// lib/cors.ts
import cors from "cors";
const allowedOrigins = [
"https://app.example.com",
"https://admin.example.com",
];
export const corsMiddleware = cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
},
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
allowedHeaders: ["Content-Type", "Authorization", "X-Request-Id"],
exposedHeaders: ["X-RateLimit-Remaining", "X-RateLimit-Reset"],
credentials: true,
maxAge: 86400, // preflight cache 24h
});
app.use(corsMiddleware);
CSRF Protection
// lib/csrf.ts
import { doubleCsrf } from "csrf-csrf";
import cookieParser from "cookie-parser";
import express from "express";
const app = express();
app.use(cookieParser(process.env.COOKIE_SECRET));
const {
generateToken,
doubleCsrfProtection,
} = doubleCsrf({
getSecret: () => process.env.CSRF_SECRET!,
cookieName: "__csrf",
cookieOptions: {
httpOnly: true,
sameSite: "strict",
secure: process.env.NODE_ENV === "production",
path: "/",
},
size: 64,
getTokenFromRequest: (req) => req.headers["x-csrf-token"] as string,
});
// Route to get CSRF token
app.get("/api/csrf-token", (req, res) => {
const token = generateToken(req, res);
res.json({ token });
});
// Apply CSRF protection to state-changing routes
app.use("/api", doubleCsrfProtection);
app.post("/api/transfer", (req, res) => {
// CSRF token is validated automatically
res.json({ success: true });
});
Express Rate Limiting
// lib/rate-limit.ts
import rateLimit from "express-rate-limit";
// Global rate limit
export const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 100,
standardHeaders: "draft-7",
legacyHeaders: false,
message: { error: "Too many requests, please try again later" },
keyGenerator: (req) => {
return req.headers["x-forwarded-for"] as string || req.ip || "unknown";
},
});
// Strict limit for auth routes
export const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 5,
standardHeaders: "draft-7",
legacyHeaders: false,
message: { error: "Too many login attempts" },
skipSuccessfulRequests: true,
});
// Usage
app.use(globalLimiter);
app.use("/api/auth", authLimiter);
Next.js Security Headers
// next.config.ts
import type { NextConfig } from "next";
const securityHeaders = [
{
key: "Content-Security-Policy",
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-eval' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob: https://cdn.example.com",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
].join("; "),
},
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
];
const nextConfig: NextConfig = {
async headers() {
return [
{
source: "/(.*)",
headers: securityHeaders,
},
];
},
};
export default nextConfig;
Next.js Middleware CORS
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
const allowedOrigins = ["https://app.example.com", "https://admin.example.com"];
export function middleware(req: NextRequest) {
const origin = req.headers.get("origin") ?? "";
const isAllowed = allowedOrigins.includes(origin);
// Handle preflight
if (req.method === "OPTIONS") {
return new NextResponse(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": isAllowed ? origin : "",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Max-Age": "86400",
},
});
}
const response = NextResponse.next();
if (isAllowed) {
response.headers.set("Access-Control-Allow-Origin", origin);
response.headers.set("Access-Control-Allow-Credentials", "true");
}
return response;
}
export const config = {
matcher: ["/api/:path*"],
};
CSP Report-Only Mode
// Test a new CSP without breaking anything
const reportOnlyHeaders = [
{
key: "Content-Security-Policy-Report-Only",
value: [
"default-src 'self'",
"script-src 'self'",
"style-src 'self'",
"report-uri /api/csp-report",
].join("; "),
},
];
// Endpoint to collect violations
// app/api/csp-report/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const report = await req.json();
console.warn("CSP Violation:", JSON.stringify(report, null, 2));
// In production, send to your logging service
return NextResponse.json({ received: true });
}
Combined Security Middleware for Express
// lib/security.ts
import helmet from "helmet";
import cors from "cors";
import rateLimit from "express-rate-limit";
import { doubleCsrf } from "csrf-csrf";
import cookieParser from "cookie-parser";
import { Express } from "express";
export function applySecurityMiddleware(app: Express) {
// Parse cookies first (required for CSRF)
app.use(cookieParser(process.env.COOKIE_SECRET));
// Helmet with custom CSP
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:"],
connectSrc: ["'self'"],
frameSrc: ["'none'"],
objectSrc: ["'none'"],
},
},
})
);
// CORS
app.use(
cors({
origin: process.env.ALLOWED_ORIGINS?.split(",") ?? [],
credentials: true,
})
);
// Global rate limit
app.use(
rateLimit({
windowMs: 15 * 60 * 1000,
limit: 200,
standardHeaders: "draft-7",
legacyHeaders: false,
})
);
// CSRF for non-GET routes
const { doubleCsrfProtection, generateToken } = doubleCsrf({
getSecret: () => process.env.CSRF_SECRET!,
cookieName: "__csrf",
cookieOptions: {
httpOnly: true,
sameSite: "strict",
secure: process.env.NODE_ENV === "production",
},
getTokenFromRequest: (req) => req.headers["x-csrf-token"] as string,
});
app.get("/api/csrf-token", (req, res) => {
res.json({ token: generateToken(req, res) });
});
app.use("/api", doubleCsrfProtection);
}
Best Practices
- Start with
Content-Security-Policy-Report-Onlyto identify violations before enforcing. Review reports for at least a week before switching to enforcing mode. - Set
Strict-Transport-SecuritywithincludeSubDomainsandpreloadon production. Submit your domain to the HSTS preload list. - Use
SameSite=StrictorSameSite=Laxon all cookies. Combine withHttpOnlyandSecureflags. - Apply rate limits per route type: strict on auth endpoints, moderate on API endpoints, lenient on static assets.
- Use
Permissions-Policyto explicitly disable browser features you do not use (camera, microphone, geolocation). - For CORS, never use
origin: "*"withcredentials: true. Browsers will reject this combination, and it signals a misconfiguration. - Use nonces rather than
'unsafe-inline'for CSP script-src when possible. This is the strongest XSS mitigation available via headers.
Anti-Patterns
- Setting
unsafe-inlineandunsafe-evalin CSP and calling it done. These directives effectively disable CSP protection against XSS. Use nonces or hashes instead. - Applying CORS with
origin: "*"in production. This allows any website to make requests to your API. Always whitelist specific origins. - Skipping CSRF protection because "we use JWTs". If the JWT is stored in a cookie (common for SSR apps), CSRF attacks are still possible. Token-in-header or SameSite cookies are needed.
- Rate limiting only on the application layer behind a load balancer. Each instance sees a fraction of traffic. Use a shared store (Redis) or an edge rate limiter.
- Disabling Helmet defaults to "fix" broken functionality. If a header breaks your app, understand why before removing it. Usually the fix is adding an exception, not removing the header entirely.
- Setting security headers only on HTML pages. API responses also need CORS, rate limit headers, and content type protection. Apply headers globally and customize per route.
- Forgetting to set
X-Content-Type-Options: nosniff. Without it, browsers may MIME-sniff responses and execute files as scripts that you intended to serve as data.
Install this skill directly: skilldb add security-ratelimit-skills
Related Skills
Arcjet
Rate limiting, bot detection, email validation, and shield attack protection using Arcjet with Next.js middleware and stacking rules
Cloudflare Turnstile
Privacy-preserving CAPTCHA alternative using Cloudflare Turnstile for bot protection with server-side verification in Next.js and Express
OWASP ZAP
Automated web application security testing, API scanning, and CI/CD DAST integration using OWASP ZAP
Snyk
Dependency vulnerability scanning, license compliance, and continuous security monitoring using Snyk CLI and CI/CD integrations
Svix
Webhook delivery infrastructure including sending webhooks, retry logic, signature verification, event types, consumer portal, and message logging with Svix
Unkey
API key management, rate limiting, usage tracking, key verification, temporary keys, ratelimit API, and analytics with Unkey