Configura headers de seguridad HTTP con Helmet en Node.js
Establece headers de seguridad HTTP en apps Express con Helmet: CSP, HSTS, X-Frame-Options, X-Content-Type-Options y CORS para seguridad web OWASP
Visión General
Cada respuesta que envía tu app Express lleva headers que protegen a los usuarios o los dejan expuestos: clickjacking, XSS y MIME sniffing explotan precisamente los que faltan. Helmet es un paquete de ~15 middleware pequeños que configura esos headers por ti, y app.use(helmet()) activa valores seguros por defecto con una línea. Esta receta cubre la configuración de Helmet 8 con Content Security Policy (CSP), HSTS, CORS y overrides por ruta para apps que cumplen OWASP.
Cuándo Usar
Usa este recurso cuando:
- Ejecutes cualquier aplicación web o API en Express.js en producción
- Despliegues que deban pasar auditorías de headers de seguridad OWASP (securityheaders.com, pentests)
- Apps que sirven HTML a navegadores, donde CSP es tu principal mitigación de XSS
- Al endurecer una API que igualmente se beneficia de
nosniff, HSTS y quitarX-Powered-By
Helmet solo funciona con Express y frameworks estilo Connect. Para Fastify usa @fastify/helmet, para Koa usa koa-helmet, y en Next.js configura los headers en next.config.js: el middleware no encaja en esos setups.
Requisitos Previos
- Node.js 18+
- Proyecto Express.js (
npm install express) - Helmet 8 (
npm install helmet@8) — las opciones de abajo usan la nomenclaturax*de v8; los nombres antiguos de v6 (frameguard,noSniff) siguen funcionando como aliases deprecated
Solución
Los snippets están listos para copiar y pegar; una app Express ejecutable con tests está en el repositorio complementario.
1. Instalar dependencias
npm install express helmet@8 cors
2. Configuración básica de Helmet
const express = require("express");
const helmet = require("helmet");
const cors = require("cors");
const app = express();
// Habilitar Helmet con headers de seguridad por defecto
app.use(helmet());
// Habilitar CORS con orígenes específicos
app.use(cors({
origin: ["https://example.com", "https://app.example.com"],
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
maxAge: 86400, // Cache preflight: 24 horas
}));
app.get("/", (req, res) => {
res.json({ message: "Secure API" });
});
app.listen(3000, () => console.log("Server running on port 3000"));
3. Content Security Policy (CSP) personalizado
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
"'self'",
"https://cdn.jsdelivr.net",
"https://www.googletagmanager.com",
],
styleSrc: [
"'self'",
"https://fonts.googleapis.com",
"'unsafe-inline'", // Requerido para muchos frameworks CSS
],
fontSrc: [
"'self'",
"https://fonts.gstatic.com",
],
imgSrc: [
"'self'",
"data:",
"https:",
],
connectSrc: [
"'self'",
"https://api.example.com",
"https://www.google-analytics.com",
],
frameAncestors: ["'none'"], // Prevenir clickjacking
baseUri: ["'self'"],
formAction: ["'self'"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
},
},
}));
4. HSTS (HTTP Strict Transport Security)
app.use(helmet({
strictTransportSecurity: {
maxAge: 31536000, // 1 año en segundos
includeSubDomains: true,
preload: true,
},
}));
5. Control fino de headers
app.use(helmet({
xFrameOptions: { action: "deny" }, // clickjacking (nombre v6: frameguard)
xContentTypeOptions: true, // nosniff — ya es el default
xPoweredBy: true, // quitar X-Powered-By (default)
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
xDnsPrefetchControl: { allow: false }, // X-DNS-Prefetch-Control (v6: dnsPrefetchControl)
xDownloadOptions: true, // X-Download-Options: noopen (v6: ieNoOpen)
xPermittedCrossDomainPolicies: { permittedPolicies: "none" },
crossOriginEmbedderPolicy: { policy: "require-corp" },
crossOriginOpenerPolicy: { policy: "same-origin" },
crossOriginResourcePolicy: { policy: "same-origin" },
originAgentCluster: true,
}));
6. Configuración de headers por ruta
// Rutas API — headers más estrictos, sin CSP
const apiHelmet = helmet({
contentSecurityPolicy: false,
xFrameOptions: { action: "deny" },
});
app.use("/api", apiHelmet);
// Rutas web — CSP completo
const webHelmet = helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://cdn.jsdelivr.net"],
},
},
});
app.use("/web", webHelmet);
7. Middleware de headers personalizados
// Headers de seguridad personalizados no cubiertos por Helmet
app.use((req, res, next) => {
// Permissions Policy (sucesor de Feature Policy)
res.setHeader(
"Permissions-Policy",
"camera=(), microphone=(), geolocation=(), payment=()"
);
// X-Robots-Tag (prevenir indexación de endpoints API)
if (req.path.startsWith("/api")) {
res.setHeader("X-Robots-Tag", "noindex, nofollow");
}
// Cache-Control para endpoints sensibles
if (req.path.includes("/auth") || req.path.includes("/user")) {
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
}
next();
});
8. Verificar headers con test
// Instalar: npm install --save-dev jest supertest
// Exporta tu app (module.exports = app) antes de requerirla aquí.
const request = require("supertest");
const app = require("./app");
describe("Security Headers", () => {
it("should set X-Content-Type-Options", async () => {
const response = await request(app).get("/");
expect(response.headers["x-content-type-options"]).toBe("nosniff");
});
it("should set X-Frame-Options", async () => {
const response = await request(app).get("/");
expect(response.headers["x-frame-options"]).toBe("DENY");
});
it("should set Strict-Transport-Security", async () => {
const response = await request(app).get("/");
expect(response.headers["strict-transport-security"]).toContain("max-age=31536000");
});
it("should set Content-Security-Policy", async () => {
const response = await request(app).get("/");
expect(response.headers["content-security-policy"]).toContain("default-src 'self'");
});
it("should remove X-Powered-By", async () => {
const response = await request(app).get("/");
expect(response.headers["x-powered-by"]).toBeUndefined();
});
});
Cómo Funciona
helmet() no es un solo middleware: es un paquete de ~15 pequeños, cada uno responsable de un header. Llamarlo sin argumentos activa los defaults de la tabla; pasar un objeto de opciones sobreescribe cada middleware de forma independiente.
Lo que helmet() configura por defecto (defaults de v8):
| Header | Valor por defecto | Qué bloquea |
|---|---|---|
Content-Security-Policy | default-src 'self' … | Fuentes XSS inline/externas |
Strict-Transport-Security | max-age=15552000 (180 días) | Downgrade a HTTP, MITM |
X-Frame-Options | SAMEORIGIN | Clickjacking vía iframes |
X-Content-Type-Options | nosniff | Ataques de MIME-sniffing |
Referrer-Policy | strict-origin-when-cross-origin | Fuga de referrer |
Cross-Origin-Embedder-Policy | require-corp | Embebido de recursos no autorizado |
Cross-Origin-Opener-Policy | same-origin | Ataques al window handle |
Cross-Origin-Resource-Policy | same-origin | Lectura cross-origin de recursos |
X-DNS-Prefetch-Control | off | DNS prefetch no deseado |
X-Download-Options | noopen | Bug de ejecución de archivos en IE8 |
X-Permitted-Cross-Domain-Policies | none | Lecturas cross-domain de Flash/PDF |
X-Powered-By | eliminado | Fingerprinting del stack |
X-XSS-Protection | 0 | Quirks del auditor legado |
Tres headers merecen cuidado extra:
- CSP hace el trabajo pesado: define desde dónde pueden cargarse scripts, estilos e imágenes, que es lo que realmente detiene el código inyectado. Un
default-src 'self'estricto también bloquea CDNs legítimos — por eso las configs de producción casi siempre necesitan entradas explícitas enscriptSrc/styleSrc, y por eso existereportOnly. - HSTS es un trinquete de una sola dirección: cuando un navegador ve
max-age, rechaza HTTP plano hacia ese dominio hasta que expire — yincludeSubDomainslo aplica también astaging.einternal..preloadva más allá y mete tu dominio en los navegadores de forma permanente; actívalo solo cuando HTTPS sea definitivamente permanente. frame-ancestorsen CSP vsX-Frame-Options: los navegadores modernos prefieren la directiva CSP e ignoran XFO cuando ambos existen. Helmet establece los dos porque el header sigue protegiendo a usuarios en motores antiguos — mantenlos, y configuraframe-ancestorscon tu intención real.
Variantes
CSP basado en nonce para scripts inline
const crypto = require("crypto");
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString("base64");
next();
});
app.use(helmet({
contentSecurityPolicy: {
useDefaults: true,
directives: {
scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
},
},
}));
// Usar nonce en templates
app.get("/", (req, res) => {
res.send(`
<script nonce="${res.locals.nonce}">
console.log("This inline script is allowed");
</script>
`);
});
CSP report-only (testing)
// Reportar violaciones sin bloquear — para testear CSP antes de aplicar
app.use(helmet({
contentSecurityPolicy: {
reportOnly: true,
directives: {
defaultSrc: ["'self'"],
reportUri: ["/api/csp-report"],
},
},
}));
app.post("/api/csp-report", express.json({ type: "application/csp-report" }), (req, res) => {
console.log("CSP Violation:", req.body);
res.status(204).end();
});
ES Modules (ESM)
Todo lo anterior usa CommonJS porque Express 4 es el default. En proyectos con "type": "module" la configuración es idéntica con sintaxis import — Helmet 8 distribuye build dual:
import express from "express";
import helmet from "helmet";
import cors from "cors";
const app = express();
app.use(helmet());
Producción vs desarrollo
const isProduction = process.env.NODE_ENV === "production";
app.use(helmet({
contentSecurityPolicy: isProduction ? {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'"],
},
} : false, // Deshabilitar CSP en dev para hot reload
strictTransportSecurity: isProduction ? {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
} : false, // Deshabilitar HSTS en dev (HTTP)
}));
Verificar tus Headers
Antes de desplegar, confirma lo que el servidor realmente envía. La comprobación más rápida: curl -I https://tuapp.com y busca los headers que configuraste.
curl -sI https://tuapp.com | grep -iE "strict-transport|x-frame|x-content|content-security"
Deberías ver algo así:
strict-transport-security: max-age=31536000; includeSubDomains; preload
x-frame-options: DENY
x-content-type-options: nosniff
content-security-policy: default-src 'self';script-src 'self' https://cdn.jsdelivr.net
Para una auditoría completa, pega tu URL en securityheaders.com: puntúa tu configuración y marca los headers que faltan. En DevTools del navegador, la pestaña Network muestra los headers de respuesta por petición, y la consola reporta violaciones CSP con la directiva exacta que se disparó.
Mejores Prácticas
- Para una baseline de seguridad más amplia, mira la guía API Security Checklist — Authentication to Encryption.
- Empieza con CSP report-only: ejecuta
reportOnly: trueunos días para recolectar violaciones antes de aplicar, o romperás assets en producción - Usa nonces para scripts inline:
'unsafe-inline'enscriptSrcneutraliza la protección XSS; los nonces por petición la mantienen estricta - Establece HSTS solo sobre HTTPS: los navegadores ignoran HSTS en respuestas HTTP, y cachearlo por accidente en dev te deja fuera de localhost
- Fija la versión de helmet en package.json: v8 renombró varias opciones, así que un upgrade mayor silencioso puede cambiar qué headers se establecen
Errores Comunes
- Usar
'unsafe-inline'enscriptSrc: permite cualquier script inline, deshaciendo la mayor parte de lo que protege CSP — usa nonces o hashes en su lugar - Olvidar deshabilitar HSTS en desarrollo: los navegadores cachean HSTS por dominio, así que
localhostde repente exige HTTPS en todos los navegadores donde probaste - Usar nombres de opciones pre-v8 a ciegas:
frameguardy compañía siguen funcionando como aliases deprecated, perocontentTypeOptionsno es una opción real — se ignora silenciosamente ynosniffsolo aplica porque es el default - No establecer
frame-ancestorsen CSP:X-Frame-Optionspor sí solo es el mecanismo legado; los navegadores modernos siguenframe-ancestors - CORS demasiado permisivo:
origin: "*"combinado concredentials: truees rechazado por los navegadores e inseguro de todos modos — usa una lista blanca de orígenes reales
Lectura Adicional
- Documentación de Helmet: referencia de opciones para cada middleware incluido, con los nombres
x*de v8. - OWASP Secure Headers Project: conjunto recomendado de headers y su fundamentación.
- MDN — Content-Security-Policy: referencia directiva por directiva.
- hstspreload.org: comprueba elegibilidad y envía tu dominio a la lista de preload de HSTS.
Preguntas frecuentes
¿Helmet hace mi app segura?
Hace seguros tus headers — es una capa. Helmet no te salva de inyección SQL, auth rota o input sin sanitizar; combínalo con validación de entrada, autenticación y rate limiting.
¿Debo usar Helmet para backends solo API?
Sí, pero recórtalo. nosniff, quitar X-Powered-By y HSTS siguen importando para APIs, mientras que CSP solo ayuda cuando sirves HTML — por eso el ejemplo por ruta deshabilita contentSecurityPolicy en /api.
¿Cómo depuro violaciones de CSP?
Activa reportOnly: true y loguea lo que vuelve — el reporte de violación nombra la directiva y la URI bloqueadas. En DevTools, los errores CSP aparecen en la consola como "Refused to load … because it violates the following Content Security Policy directive".
¿Puedo usar Helmet con Next.js?
Next.js no ejecuta middleware de Express. Configura los mismos headers en next.config.js con la función headers() — los valores de esta receta se trasladan directamente.
¿Qué versión de Helmet debo instalar?
Helmet 8. npm install helmet actualmente resuelve a v8, que renombró varias opciones a la nomenclatura x* (frameguard → xFrameOptions, noSniff → xContentTypeOptions). Los nombres antiguos siguen funcionando como aliases deprecated, pero el código nuevo debería usar los nombres de v8 que se muestran aquí.
¿Puedo establecer estos headers manualmente sin Helmet?
res.setHeader() funciona para un puñado de headers, pero reimplementarías a mano la serialización de directivas CSP, los defaults y el control por ruta. Helmet existe porque esa superficie es fácil de equivocar sutilmente.
¿Por qué se rompió mi sitio al activar CSP?
Casi siempre por 'unsafe-inline' o una entrada faltante en la lista blanca: los scripts inline, fuentes externas, analytics y assets de CDN quedan bloqueados por default-src 'self' hasta que los permitas. Compara los fallos de la pestaña Network con las listas scriptSrc/styleSrc/fontSrc/imgSrc, o ejecuta primero el modo report-only.
¿Helmet funciona con Fastify o Koa?
No directamente — es middleware de Express. Fastify tiene @fastify/helmet (mismo objeto de opciones) y Koa tiene koa-helmet (superficie de API más antigua). Los conceptos de headers son idénticos; solo cambia la integración.
¿Necesito X-Frame-Options si ya configuro frame-ancestors?
Mantén ambos. Los navegadores modernos prefieren frame-ancestors de CSP e ignoran XFO cuando está presente, pero el header sigue protegiendo a usuarios en motores antiguos — y Helmet establece los dos de todos modos.
¿Qué hace realmente el flag preload de HSTS?
Establecer preload por sí solo no cambia nada; marca tu header HSTS como elegible para envío a la lista de preload de los navegadores. Una vez aceptado, los navegadores traen tu dominio hardcodeado como solo-HTTPS — nunca ocurre una primera petición HTTP. La eliminación tarda meses, así que trátalo como permanente.
Recursos Relacionados
Asegurar APIs con HTTP Security Headers
Cómo configurar headers de seguridad esenciales como HSTS, CSP y X-Frame-Options para proteger APIs y aplicaciones web de ataques comunes.
RecipeCabeceras de seguridad HTTP
Fortalece aplicaciones web con HTTP security headers: CSP, HSTS, X-Frame-Options y una lista de verificación completa de headers de seguridad.
RecipeSanitizar Input de Usuario
Cómo sanitizar y validar input de usuario en Python, Java y JavaScript para prevenir ataques de inyección.
RecipeProteger Formularios Web Contra Ataques CSRF
Cómo prevenir ataques de Cross-Site Request Forgery usando tokens de sincronización, cookies SameSite y patrones de double-submit cookie.
GuideContent Security Policy: cabeceras CSP, nonces, hashes
Desplegá Content Security Policy sin romper tu sitio: directivas CSP, nonces, hashes, strict-dynamic, reportes e integración por framework.
GuideChecklist de Seguridad API: Autenticación a Encriptación
Checklist de seguridad esencial para APIs: autenticación, autorización, validación de entrada, rate limiting, encriptación, logging y endurecimiento de despliegue.