Why Security Headers Matter
HTTP security headers are response headers that instruct the browser to enable or disable certain security features. They form a critical layer of defense for web applications by telling browsers how to behave when handling your site's content. Without proper security headers, your application is vulnerable to clickjacking, cross-site scripting, data injection, and other browser-based attacks — even if your server-side code is secure.
Security headers are one of the easiest and most effective security improvements you can make. They require no changes to your application logic — just adding the right headers to your responses. Most can be configured at the web server or CDN level.
// Complete security headers middleware for Express
import { Request, Response, NextFunction } from "express";
function securityHeaders(req: Request, res: Response, next: NextFunction) {
// Prevent MIME type sniffing
// Without this, browsers may try to "guess" the content type,
// potentially executing a text file as JavaScript
res.setHeader("X-Content-Type-Options", "nosniff");
// Prevent clickjacking (being embedded in iframes)
// DENY: never allow framing
// SAMEORIGIN: only allow framing by same origin
res.setHeader("X-Frame-Options", "DENY");
// Force HTTPS for 2 years, include subdomains, add to preload list
res.setHeader(
"Strict-Transport-Security",
"max-age=63072000; includeSubDomains; preload"
);
// Control how much referrer information is sent
// strict-origin-when-cross-origin: send full URL for same-origin,
// only origin for cross-origin, nothing for downgrade
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
// Control browser features
res.setHeader(
"Permissions-Policy",
"camera=(), microphone=(), geolocation=(), payment=(self), usb=(), magnetometer=(), gyroscope=()"
);
// Prevent DNS prefetching (can leak which links are on the page)
res.setHeader("X-DNS-Prefetch-Control", "off");
// Prevent browsers from caching sensitive pages
// Apply to authenticated routes
if (req.path.startsWith("/api/") || req.path.startsWith("/dashboard")) {
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, private");
res.setHeader("Pragma", "no-cache");
res.setHeader("Expires", "0");
}
// Remove server identification header
res.removeHeader("X-Powered-By");
next();
}
app.use(securityHeaders);
Content-Security-Policy Header
The Content-Security-Policy (CSP) header is the most powerful security header. It deserves its own topic (covered next), but here is a brief overview. CSP controls which resources the browser is allowed to load for your page — scripts, styles, images, fonts, and more. A strict CSP can prevent most XSS attacks by blocking inline scripts and restricting which domains can serve resources.
// Basic CSP configuration
res.setHeader(
"Content-Security-Policy",
[
"default-src 'self'",
"script-src 'self' 'nonce-{RANDOM}'",
"style-src 'self' 'unsafe-inline'", // Needed for many CSS-in-JS
"img-src 'self' data: https:",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self' https://api.myapp.com",
"frame-ancestors 'none'", // Replaces X-Frame-Options
"base-uri 'self'",
"form-action 'self'",
"upgrade-insecure-requests",
].join("; ")
);
Cross-Origin Headers
Cross-origin headers control how your site interacts with resources from other origins. These headers protect against data leaks and side-channel attacks like Spectre.
// Cross-Origin headers for enhanced isolation
function crossOriginHeaders(req: Request, res: Response, next: NextFunction) {
// Cross-Origin-Opener-Policy: Isolate your browsing context
// Prevents other sites from getting a reference to your window
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
// Cross-Origin-Embedder-Policy: Require CORS for all resources
// Required for SharedArrayBuffer (used by some libraries)
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
// Cross-Origin-Resource-Policy: Control who can load your resources
// same-origin: only your site can load these resources
// same-site: your site and its subdomains
// cross-origin: anyone (for public resources like CDN assets)
res.setHeader("Cross-Origin-Resource-Policy", "same-origin");
next();
}
// For API endpoints that serve to different origins
function apiCrossOriginHeaders(req: Request, res: Response, next: NextFunction) {
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
next();
}
Testing Security Headers
// Automated security header tests
import { describe, it, expect } from "vitest";
describe("Security Headers", () => {
let headers: Headers;
beforeAll(async () => {
const response = await fetch("http://localhost:3000/");
headers = response.headers;
});
it("should set X-Content-Type-Options", () => {
expect(headers.get("x-content-type-options")).toBe("nosniff");
});
it("should set X-Frame-Options", () => {
const value = headers.get("x-frame-options");
expect(["DENY", "SAMEORIGIN"]).toContain(value);
});
it("should set Strict-Transport-Security", () => {
const hsts = headers.get("strict-transport-security");
expect(hsts).toBeTruthy();
expect(hsts).toContain("max-age=");
// Ensure max-age is at least 1 year (31536000 seconds)
const maxAge = parseInt(hsts!.match(/max-age=(\d+)/)?.[1] || "0");
expect(maxAge).toBeGreaterThanOrEqual(31536000);
});
it("should set Content-Security-Policy", () => {
expect(headers.get("content-security-policy")).toBeTruthy();
});
it("should set Referrer-Policy", () => {
const value = headers.get("referrer-policy");
expect(value).toBeTruthy();
expect(value).not.toBe("unsafe-url");
expect(value).not.toBe("no-referrer-when-downgrade");
});
it("should not expose server information", () => {
expect(headers.has("x-powered-by")).toBe(false);
expect(headers.has("server")).toBe(false);
});
it("should set Permissions-Policy", () => {
const pp = headers.get("permissions-policy");
expect(pp).toBeTruthy();
expect(pp).toContain("camera=()");
expect(pp).toContain("microphone=()");
});
});
Security Headers Quick Reference
| Header | Purpose | Recommended Value |
|---|---|---|
| X-Content-Type-Options | Prevent MIME sniffing | nosniff |
| X-Frame-Options | Prevent clickjacking | DENY |
| Strict-Transport-Security | Force HTTPS | max-age=63072000; includeSubDomains |
| Content-Security-Policy | Control resource loading | Strict policy per app needs |
| Referrer-Policy | Control referrer leaks | strict-origin-when-cross-origin |
| Permissions-Policy | Control browser APIs | Disable unused features |
Security Warning: Header Pitfalls
- CSP report-only first: Deploy CSP in report-only mode initially to avoid breaking your application.
- Test thoroughly: Security headers can break legitimate functionality. Test all features after enabling them.
- Do not use X-XSS-Protection: This legacy header is deprecated and can actually introduce vulnerabilities in some browsers.
- Scan regularly: Use securityheaders.com to verify your headers are properly configured.