typescript xml 91 lines · 3 tabs

Security headers with helmet (baseline hardening)

Shared by codesnips Jan 2026
3 tabs
import crypto from 'crypto';
import helmet from 'helmet';
import type { Express, Request, Response, NextFunction } from 'express';

function cspNonce(req: Request, res: Response, next: NextFunction): void {
  res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
  next();
}

export function applySecurityHeaders(app: Express): void {
  app.use(cspNonce);

  app.use(
    helmet({
      contentSecurityPolicy: {
        useDefaults: true,
        directives: {
          defaultSrc: ["'self'"],
          scriptSrc: [
            "'self'",
            (req, res) => `'nonce-${(res as Response).locals.cspNonce}'`,
          ],
          styleSrc: ["'self'", "'unsafe-inline'"],
          imgSrc: ["'self'", 'data:'],
          objectSrc: ["'none'"],
          baseUri: ["'self'"],
          frameAncestors: ["'none'"],
          upgradeInsecureRequests: [],
        },
      },
      hsts: {
        maxAge: 60 * 60 * 24 * 365,
        includeSubDomains: true,
        preload: true,
      },
      referrerPolicy: { policy: 'no-referrer' },
      crossOriginOpenerPolicy: { policy: 'same-origin' },
      crossOriginResourcePolicy: { policy: 'same-origin' },
      xContentTypeOptions: true,
      frameguard: { action: 'deny' },
    })
  );
}
3 files · typescript, xml Explain with highlit

This snippet shows how a baseline of HTTP security headers is applied to an Express application using helmet, with particular attention to a nonce-based Content-Security-Policy. The core idea is defense in depth: browsers enforce policies that the server declares through response headers, so shipping the right headers turns off entire classes of attacks (clickjacking, MIME sniffing, protocol downgrade, and inline-script XSS) without touching application code.

In securityHeaders.ts, helmet() is not used with its defaults alone. A cspNonce middleware generates a fresh, cryptographically random nonce per request via crypto.randomBytes and stashes it on res.locals so templates can echo it into <script nonce="...">. The CSP is then declared with directives where scriptSrc allows 'self' plus a (req, res) callback returning the per-request nonce. This is the crucial part: because the nonce is unguessable and changes every request, attacker-injected inline scripts are rejected, while legitimate first-party scripts opt in explicitly. objectSrc is locked to 'none', frameAncestors to 'none' to prevent framing, and upgradeInsecureRequests is enabled so mixed content is auto-upgraded to HTTPS.

Beyond CSP, the module configures hsts with a one-year maxAge, includeSubDomains, and preload for eligibility in the browser preload list, and sets referrerPolicy to no-referrer to avoid leaking URLs. crossOriginOpenerPolicy and crossOriginResourcePolicy harden against cross-origin leaks (Spectre-style isolation). A trade-off worth noting: HSTS preload is effectively irreversible on the timescale of browser releases, so it should only be enabled once HTTPS is guaranteed across all subdomains.

In app.ts, ordering matters — applySecurityHeaders(app) runs before routes so every response, including error responses, carries the headers. app.set('trust proxy', 1) is set because HSTS and secure cookies depend on correctly detecting HTTPS behind a load balancer.

The page.ejs tab demonstrates the consumer side: <%= cspNonce %> is rendered into the inline <script> tag, tying the template back to res.locals.cspNonce. Without the matching nonce the browser would silently block that script, which is exactly the pitfall to watch for when tightening a CSP incrementally. A Content-Security-Policy-Report-Only rollout is the safer path before enforcing.


Related snips

Share this code

Here's the card — post it anywhere.

Security headers with helmet (baseline hardening) — share card
Link copied