Security Headers with Helmet — CSP, HSTS, and XSS Protection

HTTP security headers are the first and cheapest layer of protection for any web API. They instruct the browser (and sometimes the client application) how to behave when handling responses from your server — preventing common attacks like Cross-Site Scripting (XSS), clickjacking, MIME sniffing, and insecure connections. Helmet is the de facto Express middleware for setting these headers. A single app.use(helmet()) call adds fourteen security-focused headers to every response. Understanding what each header does, why it matters, and how to configure Helmet correctly for a MEAN Stack API is the subject of this lesson.

Headers Set by Helmet (default configuration)

Header What It Does Protects Against
Content-Security-Policy Controls which resources the browser can load XSS, data injection, malicious scripts
Cross-Origin-Embedder-Policy Requires cross-origin resources to opt in to being embedded Spectre-style side-channel attacks
Cross-Origin-Opener-Policy Controls which windows can access the global object Cross-origin data leaks
Cross-Origin-Resource-Policy Prevents other origins from reading this response CSRF-style resource theft
Strict-Transport-Security (HSTS) Forces HTTPS for a specified duration Protocol downgrade attacks, SSL stripping
X-Content-Type-Options: nosniff Prevents browser MIME type sniffing MIME confusion attacks
X-DNS-Prefetch-Control: off Disables DNS prefetching Privacy leaks via DNS lookups
X-Download-Options: noopen Prevents IE from executing downloads in the origin context Drive-by download attacks (IE)
X-Frame-Options: SAMEORIGIN Controls embedding in iframes Clickjacking
X-Permitted-Cross-Domain-Policies: none Prevents Adobe Flash/Acrobat from loading cross-domain content Cross-domain data theft
X-Powered-By (removed) Removes the Express fingerprint header Information disclosure — hides server tech stack
X-XSS-Protection: 0 Disables the browser’s built-in XSS filter (recommended) XSS filter bypass vulnerabilities
Origin-Agent-Cluster Requests process isolation at origin level Spectre-style timing attacks
Referrer-Policy: no-referrer Controls what referrer info is sent with requests Privacy leaks via Referer header

Content Security Policy Directives

Directive Controls Example Value
default-src Fallback for all resource types not explicitly listed 'self'
script-src JavaScript sources 'self' 'nonce-abc123'
style-src CSS sources 'self' 'unsafe-inline'
img-src Image sources 'self' data: https://cdn.example.com
connect-src XHR, fetch, WebSocket destinations 'self' https://api.example.com
font-src Font sources 'self' https://fonts.gstatic.com
frame-ancestors Which pages can embed this in an iframe 'none'
upgrade-insecure-requests Upgrades HTTP requests to HTTPS automatically (flag, no value)
Note: For a pure REST API (no HTML pages, no served frontend), CSP is largely irrelevant because browsers do not load scripts from API responses. However, you should still configure it to restrict what the API response can do. For APIs that serve an Angular SPA from the same domain, CSP becomes critical — it controls which scripts Angular can load and where it can make fetch calls. Start with a restrictive policy and relax only what you need.
Tip: HSTS (Strict-Transport-Security) should only be enabled when your server actually serves HTTPS. On a local development server (HTTP only), enabling HSTS with a long max-age will cause browsers to refuse HTTP connections to localhost for the duration of the max-age period. Use Helmet’s hsts: false in development and enable it only in production via environment-aware configuration.
Warning: Helmet’s default CSP is quite restrictive and will break Angular’s inline styles and scripts. Angular adds inline styles for component encapsulation and sometimes uses inline scripts. You must configure scriptSrc and styleSrc directives to allow Angular’s patterns, or set contentSecurityPolicy: false for API-only servers and configure CSP separately on your nginx/CDN layer where the Angular app is served.

Complete Helmet Configuration

// ── Install ───────────────────────────────────────────────────────────────
// npm install helmet

const express = require('express');
const helmet  = require('helmet');
const app     = express();
const isProd  = process.env.NODE_ENV === 'production';

// ── Option 1: Default configuration (recommended starting point) ──────────
app.use(helmet());

// ── Option 2: Custom configuration for a MEAN Stack API ──────────────────
app.use(helmet({
    // HSTS — only enable in production with real HTTPS
    hsts: isProd
        ? { maxAge: 31536000, includeSubDomains: true, preload: true }
        : false,

    // CSP — strict config for API-only server
    contentSecurityPolicy: {
        directives: {
            defaultSrc:     ["'self'"],
            scriptSrc:      ["'self'"],
            styleSrc:       ["'self'", "'unsafe-inline'"],
            imgSrc:         ["'self'", 'data:', 'https:'],
            connectSrc:     ["'self'"],
            fontSrc:        ["'self'"],
            objectSrc:      ["'none'"],
            mediaSrc:       ["'self'"],
            frameSrc:       ["'none'"],
            frameAncestors: ["'none'"],
            upgradeInsecureRequests: isProd ? [] : null,
        },
    },

    // Referrer policy — do not leak URL info in Referer header
    referrerPolicy: { policy: 'strict-origin-when-cross-origin' },

    // Cross-Origin-Resource-Policy
    crossOriginResourcePolicy: { policy: 'cross-origin' },  // needed for CDN-served assets

    // Remove X-Powered-By (Helmet does this by default too)
    hidePoweredBy: true,
}));

// ── Verifying headers with curl ───────────────────────────────────────────
// curl -I http://localhost:3000/api/health
// Should see:
// content-security-policy: default-src 'self'; ...
// strict-transport-security: max-age=31536000; includeSubDomains; preload
// x-content-type-options: nosniff
// x-frame-options: SAMEORIGIN
// x-xss-protection: 0

// ── Environment-aware helper ──────────────────────────────────────────────
function configureHelmet() {
    const baseConfig = {
        hidePoweredBy: true,
        xContentTypeOptions: true,
        xFrameOptions: { action: 'sameorigin' },
        referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
    };

    if (process.env.NODE_ENV === 'production') {
        return helmet({
            ...baseConfig,
            hsts: { maxAge: 63072000, includeSubDomains: true, preload: true },
            contentSecurityPolicy: {
                directives: {
                    defaultSrc:  ["'self'"],
                    scriptSrc:   ["'self'"],
                    connectSrc:  ["'self'", process.env.API_URL],
                    imgSrc:      ["'self'", 'data:', process.env.CDN_URL],
                    objectSrc:   ["'none'"],
                    frameAncestors: ["'none'"],
                }
            }
        });
    }

    // Development — relaxed CSP, no HSTS
    return helmet({
        ...baseConfig,
        hsts: false,
        contentSecurityPolicy: false,
    });
}

app.use(configureHelmet());

How It Works

Step 1 — HTTP Headers Are Sent With Every Response

HTTP headers are key-value metadata sent before the response body. Security headers are directives to the browser about how to treat the response. They are set on the server side and enforced by the browser. A well-configured server sets these headers automatically for every response — not just HTML pages. REST API responses benefit too, because browsers process API responses (checking CORS, MIME types, and content policies).

Step 2 — X-Powered-By Removal Hides Stack Fingerprinting

By default, Express adds X-Powered-By: Express to every response. This header tells attackers exactly which framework you are running, making targeted attacks easier. Helmet removes it with app.disable('x-powered-by'). This is security through obscurity — not a defence on its own — but it removes one free data point for attackers.

Step 3 — HSTS Forces HTTPS Browser-Side

Once a browser receives a response with Strict-Transport-Security: max-age=31536000, it remembers that this domain requires HTTPS for the next year (31,536,000 seconds). Any attempt to connect over HTTP — even from a user typing http:// — is automatically upgraded to HTTPS by the browser itself, before the request leaves the device. This prevents protocol downgrade attacks where a man-in-the-middle strips the HTTPS upgrade redirect.

Step 4 — CSP Is a Whitelist of Allowed Resource Origins

Content Security Policy tells the browser which domains are allowed to provide scripts, styles, images, and connections. An attacker who injects a script tag like <script src="https://evil.com/steal.js"> into your HTML cannot execute it if your CSP only allows scripts from 'self'. The browser checks the CSP header and blocks the load. This makes XSS attacks significantly harder to exploit even when injection is possible.

Step 5 — nosniff Prevents MIME Confusion Attacks

X-Content-Type-Options: nosniff instructs the browser not to try to determine the file type from its content — only trust the Content-Type header. Without this, an attacker could upload a file with a JavaScript MIME type disguised as an image. The browser would detect the JavaScript content and execute it. With nosniff, the browser respects the declared type and refuses to execute content of the wrong type.

Real-World Example: Production Security Middleware Stack

// app.js — complete security middleware configuration
const express       = require('express');
const helmet        = require('helmet');
const cors          = require('cors');
const rateLimit     = require('express-rate-limit');
const mongoSanitize = require('express-mongo-sanitize');
const xss           = require('xss-clean');
const hpp           = require('hpp');

function createApp() {
    const app  = express();
    const prod = process.env.NODE_ENV === 'production';

    // ── 1. Security headers ───────────────────────────────────────────────
    app.use(helmet({
        hsts: prod ? { maxAge: 63072000, includeSubDomains: true } : false,
        contentSecurityPolicy: prod ? {
            directives: {
                defaultSrc:     ["'self'"],
                scriptSrc:      ["'self'"],
                connectSrc:     ["'self'", process.env.FRONTEND_URL].filter(Boolean),
                imgSrc:         ["'self'", 'data:', 'https:'],
                objectSrc:      ["'none'"],
                frameAncestors: ["'none'"],
            }
        } : false,
    }));

    // ── 2. Trust proxy (for correct req.ip behind nginx/load balancer) ────
    if (prod) app.set('trust proxy', 1);

    // ── 3. Body parsing with size limits ──────────────────────────────────
    app.use(express.json({ limit: '10kb' }));            // reject huge JSON payloads
    app.use(express.urlencoded({ extended: true, limit: '10kb' }));

    // ── 4. Sanitise MongoDB operators from input ───────────────────────────
    // Prevents: { "email": { "$gt": "" } } in login body
    app.use(mongoSanitize());

    // ── 5. Sanitise HTML in req.body, query, params ────────────────────────
    app.use(xss());

    // ── 6. Prevent HTTP Parameter Pollution ───────────────────────────────
    // Prevents: ?sort=createdAt&sort=password
    app.use(hpp({ whitelist: ['sort', 'fields', 'status', 'priority'] }));

    return app;
}

module.exports = createApp;

Common Mistakes

Mistake 1 — Enabling HSTS in development causing localhost to refuse HTTP

❌ Wrong — browser caches HSTS for localhost, breaks HTTP-only dev server:

app.use(helmet());   // applies HSTS to localhost in development!
// Browser now refuses http://localhost:3000 for the next year

✅ Correct — disable HSTS outside production:

app.use(helmet({ hsts: process.env.NODE_ENV === 'production' }));

Mistake 2 — Default CSP breaking Angular inline styles

❌ Wrong — Angular’s scoped styles blocked by strict CSP:

app.use(helmet());  // blocks Angular's inline style attributes
// Angular components render without any styles — UI appears broken

✅ Correct — allow ‘unsafe-inline’ for styles or disable CSP for pure API servers:

// For a pure API server (Angular served separately):
app.use(helmet({ contentSecurityPolicy: false }));
// CSP belongs on the frontend server (nginx/CDN), not the API

Mistake 3 — Not adding mongoSanitize — NoSQL injection vulnerability

❌ Wrong — attacker bypasses login with operator injection:

// Attacker sends: POST /auth/login { "email": { "$gt": "" }, "password": "anything" }
const user = await User.findOne({ email: req.body.email });
// { "$gt": "" } matches ALL users — returns first user — attacker logs in as admin!

✅ Correct — sanitise MongoDB operators from all user input:

const mongoSanitize = require('express-mongo-sanitize');
app.use(mongoSanitize());  // strips $ and . from req.body, req.query, req.params
// { "email": { "$gt": "" } } becomes { "email": {} } — no longer a valid query

Quick Reference

Header / Feature Helmet Config Protects Against
Remove X-Powered-By hidePoweredBy: true Stack fingerprinting
HSTS hsts: { maxAge: 63072000 } SSL stripping
CSP contentSecurityPolicy: { directives: {...} } XSS, injection
nosniff On by default MIME confusion
Clickjacking xFrameOptions: { action: 'deny' } Iframe embedding
NoSQL injection express-mongo-sanitize (separate) Operator injection
XSS in body xss-clean (separate) Stored XSS via user input
Param pollution hpp (separate) Duplicate query params

🧠 Test Yourself

An attacker sends POST /auth/login with body { "email": { "$gt": "" }, "password": "x" }. Which middleware prevents this MongoDB operator injection?