TechLead
Lesson 13 of 22
5 min read
Cybersecurity

Security Headers

Implement HTTP security headers to protect against clickjacking, XSS, MIME sniffing, and other browser-based attacks

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-OptionsPrevent MIME sniffingnosniff
X-Frame-OptionsPrevent clickjackingDENY
Strict-Transport-SecurityForce HTTPSmax-age=63072000; includeSubDomains
Content-Security-PolicyControl resource loadingStrict policy per app needs
Referrer-PolicyControl referrer leaksstrict-origin-when-cross-origin
Permissions-PolicyControl browser APIsDisable 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.

Continue Learning