Free Ebook cover Express.js Beyond Basics: Middleware, Architecture, and Maintainability

Express.js Beyond Basics: Middleware, Architecture, and Maintainability

New course

13 pages

Security Hardening in Express.js: Headers, Input Safety, and Policies

Capítulo 8

Estimated reading time: 10 minutes

+ Exercise

Production Security Hardening: What You’re Configuring (and Why)

In Express, “security hardening” is mostly about configuring edge behavior: what headers you emit, what origins you allow, how you parse input, and what you trust about the network path. The goal is to reduce the attack surface without breaking legitimate clients. This chapter focuses on Express configuration and middleware choices you can apply consistently across environments.

Checklist: Security Headers (Helmet-style)

1) Set baseline security headers

Concept: Security headers instruct browsers to reduce risky behaviors (sniffing, framing, mixed content) and to enforce transport/security policies. A Helmet-style setup is a convenient bundle, but you should still understand what you’re enabling.

Step-by-step:

  • Enable a baseline header set.
  • Customize CSP and cross-origin policies based on whether you serve HTML or only JSON APIs.
  • Verify with browser devtools and an HTTP header scanner in staging.
import express from "express";import helmet from "helmet";const app = express();app.use(helmet({  // Keep defaults, then tune the policies below as needed}));

2) Content Security Policy (CSP) for browser-facing apps

Concept: CSP reduces XSS impact by restricting where scripts/styles/images can load from. If your Express app serves HTML, CSP is one of the most effective mitigations. If you serve only JSON to non-browser clients, CSP is usually unnecessary (but harmless).

Practical guidance: Start strict, then loosen only what you must. Avoid 'unsafe-inline' for scripts; prefer nonces/hashes if you render HTML.

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

app.use(helmet.contentSecurityPolicy({  directives: {    defaultSrc: ["'self'"],    scriptSrc: ["'self'"],    styleSrc: ["'self'"],    imgSrc: ["'self'", "data:"],    objectSrc: ["'none'"],    baseUri: ["'self'"],    frameAncestors: ["'none'"]  }}));

3) Clickjacking protection (frame-ancestors / frameguard)

Concept: Prevent your pages from being embedded in iframes on other sites (clickjacking). Modern approach is CSP frame-ancestors; legacy is X-Frame-Options.

// If you don't use CSP frame-ancestors, at least do:app.use(helmet.frameguard({ action: "deny" }));

4) Disable MIME sniffing

Concept: X-Content-Type-Options: nosniff reduces content-type confusion attacks.

app.use(helmet.noSniff());

5) Referrer policy

Concept: Controls how much referrer information is sent on outbound navigation. Helps reduce leakage of sensitive URL data.

app.use(helmet.referrerPolicy({ policy: "no-referrer" }));

6) HSTS (only when HTTPS is guaranteed)

Concept: HSTS tells browsers to use HTTPS for future requests. Misconfiguration can lock users out if you’re not consistently serving HTTPS.

Step-by-step:

  • Enable only in production behind HTTPS.
  • Start with a small maxAge, then increase after validation.
  • Consider includeSubDomains only if all subdomains are HTTPS-ready.
const isProd = process.env.NODE_ENV === "production";if (isProd) {  app.use(helmet.hsts({    maxAge: 60 * 60 * 24 * 30, // 30 days to start    includeSubDomains: false,    preload: false  }));}

Checklist: CORS Policy Design (per environment)

7) Treat CORS as an allowlist, not a switch

Concept: CORS is a browser enforcement mechanism. A permissive CORS policy doesn’t “secure” your API; it increases who can call it from browsers. Design it as an allowlist of trusted frontends per environment.

Step-by-step:

  • Define allowed origins per environment (dev/staging/prod).
  • Allow only required methods and headers.
  • Decide whether credentials (cookies) are needed; if yes, you must not use wildcard origins.
import cors from "cors";const env = process.env.NODE_ENV || "development";const allowedOriginsByEnv = {  development: ["http://localhost:5173", "http://localhost:3000"],  staging: ["https://staging.example.com"],  production: ["https://app.example.com"]};const allowedOrigins = allowedOriginsByEnv[env] ?? [];app.use(cors({  origin(origin, cb) {    // Allow non-browser clients with no Origin header (e.g., curl, server-to-server)    if (!origin) return cb(null, true);    if (allowedOrigins.includes(origin)) return cb(null, true);    return cb(new Error("CORS: origin not allowed"));  },  methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],  allowedHeaders: ["Content-Type", "Authorization"],  credentials: true,  maxAge: 600}));

8) Handle preflight requests intentionally

Concept: Browsers send OPTIONS preflight requests for certain cross-origin calls. If you don’t handle them, you’ll see confusing failures.

// Ensure preflight is handled for all routesapp.options("*", cors());

9) Don’t reflect the Origin header blindly

Concept: A common misconfiguration is reflecting any incoming Origin back as Access-Control-Allow-Origin. That effectively allows any website to make credentialed requests if credentials: true is enabled.

Practical rule: If you use cookies/sessions, use a strict allowlist and return a specific origin, never *.

Checklist: Cookies, Sessions, and Request Context

10) Prefer stateless auth for APIs; if using cookies, harden them

Concept: Cookies are automatically sent by browsers, which makes CSRF a concern for state-changing requests. If you use cookies for auth/session, set flags that reduce theft and cross-site leakage.

Step-by-step cookie settings:

  • httpOnly: prevents JavaScript from reading the cookie (mitigates token theft via XSS).
  • secure: only send over HTTPS (required in modern browsers for SameSite=None).
  • sameSite: controls cross-site sending behavior.
  • domain/path: scope narrowly.
app.set("trust proxy", 1); // if behind a proxy/ingress and you need secure cookiesapp.use((req, res, next) => {  res.cookie("session", "...", {    httpOnly: true,    secure: process.env.NODE_ENV === "production",    sameSite: "lax", // consider "strict" for highly sensitive apps; "none" for cross-site embeds    path: "/"  });  next();});

11) If you use server sessions, configure store and cookie flags

Concept: In-memory session stores are not production-ready (memory leaks, no sharing across instances). Use a durable store (Redis, database) and set session cookie flags.

import session from "express-session";app.set("trust proxy", 1);app.use(session({  name: "sid",  secret: process.env.SESSION_SECRET,  resave: false,  saveUninitialized: false,  cookie: {    httpOnly: true,    secure: process.env.NODE_ENV === "production",    sameSite: "lax"  },  // store: new RedisStore({ client: redisClient })}));

12) CSRF note for cookie-based auth

Concept: If authentication relies on cookies, browsers will attach them automatically. For state-changing routes, you typically need CSRF defenses (token or double-submit cookie) and/or strict SameSite plus origin checks.

Express configuration angle: Ensure your CORS policy, cookie SameSite, and any origin/referer checks are consistent. Avoid enabling cross-site cookies unless you truly need them.

Checklist: Safe JSON Parsing and Body Limits

13) Set explicit JSON size limits

Concept: Unbounded request bodies can cause memory pressure and denial of service. Express’s JSON parser supports limits—set them deliberately based on your API needs.

Step-by-step:

  • Pick a default limit (e.g., 100kb–1mb for typical JSON APIs).
  • Use route-specific larger limits only where needed (file uploads should not go through JSON).
  • Fail fast with clear errors.
// Global defaultapp.use(express.json({ limit: "200kb", strict: true }));app.use(express.urlencoded({ extended: false, limit: "50kb" }));// Route-specific override for a known large payloadapp.post("/webhooks/provider", express.json({ limit: "2mb" }), handler);

14) Enforce correct content types for JSON endpoints

Concept: Accepting unexpected content types can lead to ambiguous parsing behavior and security gaps.

function requireJson(req, res, next) {  if (req.method === "GET") return next();  if (!req.is("application/json")) {    return res.status(415).json({ error: "Content-Type must be application/json" });  }  next();}app.use("/api", requireJson);

15) Handle JSON parse errors predictably

Concept: Malformed JSON should not crash the process. Ensure your error handler maps body-parser syntax errors to 400 responses.

app.use((err, req, res, next) => {  if (err instanceof SyntaxError && "body" in err) {    return res.status(400).json({ error: "Invalid JSON" });  }  next(err);});

Checklist: Reflected Input Handling (Express-focused)

16) Don’t reflect untrusted input into HTML responses

Concept: Reflected XSS often happens when you embed query/path values into HTML without escaping. Even if your app is “mostly API,” health pages, error pages, or debug endpoints can accidentally reflect input.

Practical guidance:

  • Prefer JSON responses for errors and diagnostics.
  • If you must render HTML, escape output via your template engine’s escaping features and avoid concatenating strings.
  • Never put raw query values into Location headers or HTML without validation.
// Safer: return JSON rather than reflecting user input in HTMLapp.get("/search", (req, res) => {  res.json({ q: String(req.query.q || "") });});

17) Avoid echoing request headers in responses

Concept: Reflecting headers like Origin, Host, or X-Forwarded-* into responses can enable cache poisoning or header-based injection issues. Keep diagnostics behind auth and never mirror raw values into HTML.

Checklist: Trust Proxy and Network Edge Settings

18) Configure trust proxy correctly (especially behind load balancers)

Concept: Express uses trust proxy to decide whether to trust X-Forwarded-Proto/X-Forwarded-For. This affects req.ip, req.secure, and secure cookie behavior. Misconfiguration can cause incorrect HTTPS detection and security decisions.

Step-by-step:

  • If you are behind a single reverse proxy (common in PaaS), set app.set('trust proxy', 1).
  • If you are not behind a proxy, leave it disabled (default).
  • If you have a known proxy subnet, configure by IP range for tighter control.
// Common case: one proxy hop (nginx/ingress)app.set("trust proxy", 1);// Tighter: trust only specific proxy IPs (example)// app.set("trust proxy", "10.0.0.0/8");

19) Enforce HTTPS at the edge; optionally redirect in-app safely

Concept: HTTPS should be enforced by your reverse proxy/CDN. If you also redirect HTTP to HTTPS in Express, do it only when trust proxy is correct; otherwise you can create redirect loops or downgrade issues.

function requireHttps(req, res, next) {  const isProd = process.env.NODE_ENV === "production";  if (!isProd) return next();  if (req.secure) return next();  // Avoid trusting Host blindly if you can; prefer a configured canonical host  const host = process.env.CANONICAL_HOST;  return res.redirect(301, `https://${host}${req.originalUrl}`);}app.use(requireHttps);

Checklist: Secure Redirects and URL Handling

20) Prevent open redirects

Concept: Open redirects happen when you redirect to a URL controlled by user input (e.g., ?next=...). Attackers use this for phishing and token leakage.

Step-by-step:

  • Allow only relative paths (starting with /).
  • Optionally maintain an allowlist of known paths.
  • Never allow full URLs unless you validate hostnames strictly.
function safeNext(nextUrl) {  if (typeof nextUrl !== "string") return "/";  // allow only relative paths  if (!nextUrl.startsWith("/")) return "/";  // prevent protocol-relative URLs like //evil.com  if (nextUrl.startsWith("//")) return "/";  return nextUrl;}app.get("/login/callback", (req, res) => {  const nextUrl = safeNext(req.query.next);  res.redirect(302, nextUrl);});

21) Normalize and validate host usage

Concept: Building absolute URLs from req.headers.host can be dangerous if Host header is spoofed (depending on your infrastructure). Prefer a configured canonical host for redirects and links.

const CANONICAL_ORIGIN = process.env.CANONICAL_ORIGIN; // e.g., https://app.example.comapp.get("/absolute-link", (req, res) => {  res.json({ url: `${CANONICAL_ORIGIN}/path` });});

Checklist: Reduce Information Leakage

22) Disable X-Powered-By

Concept: This header advertises Express. It’s not a vulnerability by itself, but removing it reduces fingerprinting.

app.disable("x-powered-by");

23) Control caching for sensitive responses

Concept: Responses containing personal data or tokens should not be cached by browsers or intermediaries.

function noStore(req, res, next) {  res.setHeader("Cache-Control", "no-store");  next();}app.use("/api", noStore);

Checklist: Operational Guards (Express-level)

24) Basic rate limiting for public endpoints

Concept: Rate limiting reduces brute force and abuse. Apply stricter limits to login, password reset, and token endpoints.

import rateLimit from "express-rate-limit";const authLimiter = rateLimit({  windowMs: 15 * 60 * 1000,  limit: 20,  standardHeaders: true,  legacyHeaders: false});app.use("/auth", authLimiter);

25) Ensure consistent error shapes without leaking internals

Concept: In production, avoid returning stack traces or internal error messages. Keep detailed errors in logs, not in responses. (This complements your centralized error handler without re-implementing it here.)

// Example toggle used by your existing error handler logicapp.set("env", process.env.NODE_ENV || "development");

Quick Reference Table: What to Enable Where

ItemDevelopmentProductionNotes
Helmet baselineYesYesTune CSP/cross-origin policies as needed
HSTSNoYes (HTTPS only)Start with small maxAge
CORS allowlistYes (localhost)Yes (strict)Never reflect Origin with credentials
JSON body limitYesYesRoute-specific overrides for webhooks
trust proxyDependsOften yesRequired for secure cookies behind proxies
Secure cookiesOptionalYesRequires HTTPS and correct trust proxy

Now answer the exercise about the content:

When an Express app uses cookie-based authentication and enables CORS with credentials, which approach best prevents unintentionally allowing any website to make authenticated browser requests?

You are right! Congratulations, now go to the next page

You missed! Try again.

With credentials: true, allowing * or reflecting any Origin can effectively permit any site to make credentialed requests. A strict per-environment allowlist returns only trusted origins.

Next chapter

Rate Limiting, Throttling, and Abuse Prevention

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.