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) |
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.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 |