Security Headers: Cómo Verificar y Configurar los Headers de Seguridad de tu Sitio
Guía práctica para verificar y configurar security headers en tu sitio web. HSTS, CSP, X-Frame-Options y más con ejemplos para Next.js y Vercel.
Security Headers: Cómo Verificar y Configurar los Headers de Seguridad de tu Sitio
Tu aplicación puede funcionar perfectamente, pero sin security headers estás dejando puertas abiertas. Cada respuesta HTTP que tu servidor envía puede incluir headers que le dicen al navegador cómo manejar tu contenido de forma segura. La mayoría de los desarrolladores los ignoran porque no rompen nada visible, hasta que alguien explota la brecha.
Los security headers no requieren librerías adicionales, no afectan el rendimiento y protegen a todos tus usuarios desde la primera visita. Son la relación costo-beneficio más alta que puedes implementar en seguridad web.
En esta guía vas a aprender a verificar qué headers tiene tu sitio actualmente, entender qué hace cada uno y configurarlos con código listo para copiar en Next.js y Vercel.
¿Qué son los security headers?
Los security headers son instrucciones HTTP que tu servidor envía al navegador junto con cada respuesta. Le dicen al navegador qué puede y qué no puede hacer cuando carga tu sitio.
Sin ellos, el navegador asume que todo está permitido: cargar scripts de cualquier dominio, embeber tu sitio en un iframe ajeno, enviar la URL completa como referrer a sitios externos.
Con ellos, defines reglas claras:
- Qué dominios pueden servir scripts, estilos e imágenes
- Si tu sitio puede ser embebido en un iframe
- Si el navegador debe forzar HTTPS siempre
- Qué APIs del navegador puede usar tu página
- Cuánta información de referencia se comparte con otros sitios
Los ataques que previenen son concretos: XSS (inyección de scripts maliciosos), clickjacking (tu sitio invisible dentro de un iframe), MIME sniffing (el navegador ejecuta archivos con tipo incorrecto), ataques de downgrade (forzar HTTP en lugar de HTTPS) y fuga de información (URLs con datos sensibles enviadas como referrer).
No cuestan nada. Se configuran una vez. Protegen a todos los usuarios.
Cómo verificar tus headers actuales
Antes de configurar algo, necesitas saber qué tienes. Hay tres formas de verificar los security headers de tu sitio.
Desde la terminal con curl
La forma más directa. Ejecuta esto en tu terminal:
curl -I https://tu-sitio.comEl flag -I hace un request HEAD, que devuelve solo los headers sin el body. La respuesta se ve así:
HTTP/2 200
content-type: text/html; charset=utf-8
strict-transport-security: max-age=63072000; includeSubDomains; preload
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline'
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-originLo que buscas son los headers de seguridad en la lista. Si no aparecen, no los tienes configurados.
Para filtrar solo los headers relevantes:
curl -sI https://tu-sitio.com | grep -iE "strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy|cross-origin"Si el resultado está vacío o le faltan líneas, tienes trabajo por hacer.
curl vs navegador
curl te muestra los headers exactos que envía el servidor. El navegador puede modificar o agregar headers por su cuenta. Para verificar la configuración del servidor, curl es la fuente de verdad.
Desde el navegador con DevTools
Si prefieres una interfaz visual:
- Abre tu sitio en el navegador
- Abre DevTools (F12 o Cmd+Option+I en Mac)
- Ve a la pestaña Network
- Recarga la página (Cmd+R o F5)
- Haz click en la primera petición (el documento HTML principal)
- Busca la sección Response Headers
Ahí vas a ver todos los headers que el servidor envió. Los de seguridad se mezclan con headers estándar como content-type y cache-control, así que revisa con cuidado.
La ventaja de DevTools es que puedes ver los headers de cada recurso individual (scripts, imágenes, fonts), no solo del documento principal. Esto es útil para verificar que los headers se aplican a todas las rutas.
Con herramientas online
La ventaja de las herramientas online sobre curl es que interpretan los valores. No solo te dicen si el header existe, sino si el valor es correcto y si hay configuraciones subóptimas.
Verifica los headers de tu sitio
Verificador de headers de seguridad gratuito -- Ingresa tu URL y te muestra qué headers tienes, cuáles faltan y qué valores deberían tener.
Los 8 headers esenciales
Estos son los security headers que todo sitio debería tener. Para cada uno: qué hace, qué ataque previene, el valor recomendado y cómo configurarlo.
1. Strict-Transport-Security (HSTS)
Qué hace: Le dice al navegador que siempre use HTTPS para conectarse a tu sitio, incluso si el usuario escribe http:// en la barra de direcciones.
Qué previene: Ataques de downgrade. Sin HSTS, la primera conexión HTTP (antes del redirect a HTTPS) no está encriptada. Un atacante en la misma red puede interceptar ese request inicial y hacer un man-in-the-middle.
Valor recomendado:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preloadLos parámetros:
max-age=63072000-- El navegador recuerda usar HTTPS durante 2 años (en segundos)includeSubDomains-- Aplica a todos los subdominiospreload-- Permite incluir tu dominio en la lista de precarga HSTS de los navegadores
Configuración en Next.js:
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
}Cuidado con includeSubDomains
Si tienes subdominios que no soportan HTTPS (un servidor de desarrollo interno, por ejemplo), includeSubDomains los va a romper. Verifica que todos tus subdominios tengan certificados SSL válidos antes de activar esta directiva. Si no estás seguro, empieza sin ella.
2. Content-Security-Policy (CSP)
Qué hace: Define una whitelist de fuentes desde donde el navegador puede cargar recursos (scripts, estilos, imágenes, fonts, conexiones). Todo lo que no esté en la lista se bloquea.
Qué previene: XSS (Cross-Site Scripting) e inyección de contenido. Si un atacante logra inyectar un script que apunta a https://evil.com/malware.js, CSP lo bloquea porque ese dominio no está en la whitelist.
Valor recomendado (básico):
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requestsConfiguración en Next.js:
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self'",
"frame-src 'none'",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'",
"upgrade-insecure-requests",
].join('; '),
}CSP es el header más poderoso y el más propenso a romper cosas. Tiene su propia sección más adelante en esta guía.
3. X-Content-Type-Options
Qué hace: Previene que el navegador adivine el tipo de contenido de un archivo (MIME sniffing).
Qué previene: Ejecución de archivos maliciosos. Sin este header, si alguien sube un archivo .txt con contenido JavaScript, el navegador puede interpretarlo como script y ejecutarlo.
Valor recomendado:
X-Content-Type-Options: nosniffSolo tiene un valor posible: nosniff. Le dice al navegador: "usa el Content-Type que yo te digo, no intentes adivinar".
Configuración en Next.js:
{
key: 'X-Content-Type-Options',
value: 'nosniff',
}Es el header más fácil de implementar. No tiene efectos secundarios. No hay razón para no tenerlo.
4. X-Frame-Options
Qué hace: Previene que otros sitios web pongan tu página dentro de un iframe.
Qué previene: Clickjacking. Un atacante pone tu sitio invisible sobre otro contenido para que el usuario haga click en botones de tu sitio sin saberlo (por ejemplo, el botón de "Transferir fondos" de un banco).
Valores posibles:
X-Frame-Options: DENY // No se puede embeber en ningún iframe
X-Frame-Options: SAMEORIGIN // Solo se puede embeber desde el mismo dominioValor recomendado: DENY para la mayoría de sitios. Usa SAMEORIGIN solo si necesitas embeber tu propio contenido en iframes.
Configuración en Next.js:
{
key: 'X-Frame-Options',
value: 'DENY',
}X-Frame-Options vs frame-ancestors
X-Frame-Options es el header legacy. La directiva frame-ancestors de CSP es la versión moderna y más flexible. Usa ambos para compatibilidad con navegadores antiguos. Si hay conflicto, CSP tiene prioridad en navegadores modernos.
5. Referrer-Policy
Qué hace: Controla cuánta información se envía en el header Referer cuando un usuario navega desde tu sitio a otro.
Qué previene: Fuga de información. Por defecto, el navegador envía la URL completa como referrer, que puede incluir datos sensibles en query params (tokens, IDs de sesión, parámetros de búsqueda).
Valores comunes:
| Valor | Qué envía |
|---|---|
no-referrer | Nada |
origin | Solo el dominio (https://tu-sitio.com) |
strict-origin | Solo dominio, solo HTTPS a HTTPS |
strict-origin-when-cross-origin | URL completa al mismo sitio, solo dominio a otros |
no-referrer-when-downgrade | No envía de HTTPS a HTTP (default del navegador) |
Valor recomendado:
Referrer-Policy: strict-origin-when-cross-originEste valor es el más equilibrado. Envía la URL completa en navegación interna (útil para analytics), solo el dominio a sitios externos con HTTPS, y nada a sitios HTTP.
Configuración en Next.js:
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
}6. Permissions-Policy
Qué hace: Controla qué APIs del navegador puede usar tu sitio. Cámara, micrófono, geolocalización, acelerómetro, giroscopio, USB, Bluetooth, Payment API y más.
Qué previene: Acceso no autorizado a APIs sensibles. Si tu sitio no usa la cámara, no hay razón para que un script inyectado pueda acceder a ella.
Valor recomendado:
Permissions-Policy: camera=(), microphone=(), geolocation=(), browsing-topics=(), interest-cohort=(), payment=(), usb=(), bluetooth=(), accelerometer=(), gyroscope=()El formato es feature=(allowlist):
()-- desactivado para todos(self)-- solo tu dominio(self "https://permitido.com")-- tu dominio y un dominio específico*-- permitido para todos
Configuración en Next.js:
{
key: 'Permissions-Policy',
value: [
'camera=()',
'microphone=()',
'geolocation=()',
'browsing-topics=()',
'interest-cohort=()',
'payment=()',
'usb=()',
'bluetooth=()',
'accelerometer=()',
'gyroscope=()',
].join(', '),
}Si tu sitio sí necesita alguna API (por ejemplo, geolocalización para un mapa), cámbiala a (self):
'geolocation=(self)', // Solo tu sitio puede pedir ubicación
'camera=(self)', // Solo tu sitio puede usar la cámara7. X-XSS-Protection
Qué hace: Activaba el filtro XSS integrado en navegadores antiguos (IE, Chrome antiguo).
Qué previene: Ataques XSS básicos en navegadores que soportan el filtro.
Valor recomendado:
X-XSS-Protection: 0Esto no es un error. El valor recomendado actual es 0 (desactivado).
Los filtros XSS de los navegadores fueron deprecados porque en algunos casos introducían nuevas vulnerabilidades. Chrome lo removió en 2019. Los navegadores modernos usan CSP en su lugar.
Configuración en Next.js:
{
key: 'X-XSS-Protection',
value: '0',
}No uses X-XSS-Protection: 1; mode=block
Aunque parece contraintuitivo, activar este header en navegadores modernos puede crear vulnerabilidades que no existían antes. El filtro XSS fue removido de todos los navegadores principales. La protección real contra XSS viene de Content-Security-Policy.
8. Cross-Origin-Opener-Policy (COOP)
Qué hace: Aísla tu ventana del navegador de otras ventanas que la abrieron o que tu sitio abrió. Previene que ventanas cross-origin accedan al objeto window de tu sitio.
Qué previene: Ataques de side-channel como Spectre, donde un atacante puede leer datos de la memoria del proceso de tu sitio si comparten el mismo proceso del navegador. También previene ataques de window manipulation donde un sitio malicioso abre tu sitio y manipula el objeto window.opener.
Valor recomendado:
Cross-Origin-Opener-Policy: same-originValores posibles:
| Valor | Comportamiento |
|---|---|
unsafe-none | Sin restricción (default) |
same-origin | Solo ventanas del mismo origen pueden interactuar |
same-origin-allow-popups | Permite popups pero aísla del opener |
Configuración en Next.js:
{
key: 'Cross-Origin-Opener-Policy',
value: 'same-origin',
}COOP y autenticación con popups
Si tu sitio usa autenticación con popups (OAuth con Google, por ejemplo), same-origin puede romper el flujo. En ese caso usa same-origin-allow-popups para permitir que el popup de autenticación se comunique con tu ventana principal.
Configurar headers en Next.js
Con todos los headers explicados, esta es la configuración completa para un proyecto Next.js. Copia y pega directamente en tu next.config.ts:
// next.config.ts
import type { NextConfig } from 'next'
const securityHeaders = [
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self'",
"frame-src 'none'",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'",
"upgrade-insecure-requests",
].join('; '),
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'Permissions-Policy',
value: [
'camera=()',
'microphone=()',
'geolocation=()',
'browsing-topics=()',
'interest-cohort=()',
'payment=()',
'usb=()',
'bluetooth=()',
'accelerometer=()',
'gyroscope=()',
].join(', '),
},
{
key: 'X-XSS-Protection',
value: '0',
},
{
key: 'Cross-Origin-Opener-Policy',
value: 'same-origin',
},
]
const config: NextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: securityHeaders,
},
]
},
}
export default configEsta configuración aplica todos los headers a todas las rutas de tu aplicación. El pattern /(.*) hace match con cualquier ruta, incluyendo la raíz.
CSP con unsafe-inline y unsafe-eval
La configuración de arriba usa 'unsafe-inline' y 'unsafe-eval' en script-src para compatibilidad con Next.js. Esto debilita CSP significativamente. Para una protección real, implementa CSP con nonces usando middleware. La sección de CSP paso a paso más abajo explica cómo hacerlo.
Configurar headers en Vercel
Si tu proyecto está en Vercel, los headers de next.config.ts se aplican automáticamente con cada deploy. No necesitas configuración adicional en Vercel.
Pero si prefieres o necesitas configurar headers a nivel de plataforma, puedes hacerlo en vercel.json:
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Strict-Transport-Security",
"value": "max-age=63072000; includeSubDomains; preload"
},
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "Referrer-Policy",
"value": "strict-origin-when-cross-origin"
},
{
"key": "Permissions-Policy",
"value": "camera=(), microphone=(), geolocation=(), browsing-topics=(), interest-cohort=()"
},
{
"key": "X-XSS-Protection",
"value": "0"
},
{
"key": "Cross-Origin-Opener-Policy",
"value": "same-origin"
}
]
}
]
}La recomendación es mantener los headers en next.config.ts por tres razones:
- El código viaja con tu proyecto. Si migras de Vercel a otra plataforma, los headers siguen funcionando
- Control de versiones. Los cambios en headers se registran en git con el resto del código
- Un solo lugar. No tienes que buscar la configuración en dos archivos diferentes
vercel.json es útil para headers que son específicos del entorno de producción o para proyectos que no son Next.js. Para más detalles sobre el deploy, revisa la guía de deploy de Next.js en Vercel.
Prioridad de headers en Vercel
Si defines el mismo header en next.config.ts y en vercel.json, el de next.config.ts tiene prioridad. No los dupliques para evitar confusiones.
Configurar CSP paso a paso
Content-Security-Policy merece su propia sección porque es el header más difícil de configurar correctamente. Un CSP mal configurado puede romper tu sitio o dar una falsa sensación de seguridad.
Paso 1: Empezar con Report-Only
Antes de bloquear algo, necesitas saber qué se va a romper. Usa Content-Security-Policy-Report-Only en lugar de Content-Security-Policy:
{
key: 'Content-Security-Policy-Report-Only',
value: [
"default-src 'self'",
"script-src 'self'",
"style-src 'self'",
"img-src 'self'",
"font-src 'self'",
"connect-src 'self'",
"frame-src 'none'",
"object-src 'none'",
].join('; '),
}Con Report-Only, el navegador reporta las violaciones en la consola de DevTools pero no bloquea nada. Tu sitio sigue funcionando mientras tú ves qué recursos se cargan desde dominios externos.
Abre DevTools, navega por tu sitio y busca mensajes con el formato:
[Report Only] Refused to load the script 'https://cdn.ejemplo.com/script.js'
because it violates the following Content Security Policy directive: "script-src 'self'"Cada mensaje te dice exactamente qué dominio necesitas agregar a la directiva correspondiente.
Paso 2: Política base
Esta es una plantilla de CSP para un sitio Next.js típico con servicios comunes:
const ContentSecurityPolicy = [
// Default: solo recursos del mismo origen
"default-src 'self'",
// Scripts: self + inline necesario para Next.js hydration
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
// Estilos: self + inline para Tailwind y CSS-in-JS
"style-src 'self' 'unsafe-inline'",
// Imágenes: self + data URIs + cualquier HTTPS
"img-src 'self' data: https:",
// Fonts: self + Google Fonts CDN
"font-src 'self' https://fonts.gstatic.com",
// Conexiones: self + tu API + analytics
"connect-src 'self'",
// No permitir iframes dentro de tu sitio
"frame-src 'none'",
// No permitir que otros sitios te embeden
"frame-ancestors 'none'",
// Restringir base URI
"base-uri 'self'",
// Restringir destinos de formularios
"form-action 'self'",
// No permitir plugins
"object-src 'none'",
// Forzar HTTPS en recursos
"upgrade-insecure-requests",
].join('; ')Paso 3: Agregar excepciones para servicios externos
Los servicios de terceros necesitan sus dominios en la directiva correcta. Estos son los más comunes:
Google Fonts:
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;Google Analytics / Tag Manager:
script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com;
connect-src 'self' https://www.google-analytics.com https://analytics.google.com;
img-src 'self' data: https://www.google-analytics.com;Vercel Analytics y Speed Insights:
script-src 'self' 'unsafe-inline' https://va.vercel-scripts.com;
connect-src 'self' https://vitals.vercel-insights.com;Imágenes externas (Cloudinary, S3, Unsplash):
img-src 'self' data: https://res.cloudinary.com https://images.unsplash.com https://tu-bucket.s3.amazonaws.com;YouTube embeds:
frame-src https://www.youtube.com https://www.youtube-nocookie.com;Paso 4: CSP con nonces (protección real)
Las directivas 'unsafe-inline' y 'unsafe-eval' debilitan CSP significativamente. Para protección real, usa nonces. Un nonce es un valor aleatorio que se genera por request y se añade tanto al header CSP como a cada tag <script>:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const csp = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'nonce-${nonce}'`,
"img-src 'self' data: https:",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self'",
"frame-src 'none'",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'",
"upgrade-insecure-requests",
].join('; ')
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-nonce', nonce)
const response = NextResponse.next({
request: { headers: requestHeaders },
})
response.headers.set('Content-Security-Policy', csp)
return response
}
export const config = {
matcher: [
{
source: '/((?!_next/static|_next/image|favicon.ico).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
],
}Y en tu layout, lee el nonce y pásalo a los scripts:
// app/layout.tsx
import { headers } from 'next/headers'
import Script from 'next/script'
export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const headersList = await headers()
const nonce = headersList.get('x-nonce') ?? ''
return (
<html lang="es">
<body>
{children}
<Script
src="https://analytics.ejemplo.com/script.js"
nonce={nonce}
strategy="afterInteractive"
/>
</body>
</html>
)
}Con nonces, el navegador solo ejecuta scripts que tengan el nonce correcto. Cualquier script inyectado por un atacante se bloquea porque no tiene el nonce de ese request.
Directivas CSP más usadas
Para referencia rápida:
| Directiva | Controla | Ejemplo |
|---|---|---|
default-src | Fuente por defecto para todo | 'self' |
script-src | Scripts JavaScript | 'self' 'nonce-abc123' |
style-src | Hojas de estilo CSS | 'self' 'unsafe-inline' |
img-src | Imágenes | 'self' data: https: |
font-src | Fuentes tipográficas | 'self' https://fonts.gstatic.com |
connect-src | Fetch, XHR, WebSocket | 'self' https://api.ejemplo.com |
frame-src | Iframes cargados por tu sitio | 'none' |
frame-ancestors | Quién puede embeber tu sitio | 'none' |
base-uri | Tags <base> | 'self' |
form-action | Destinos de formularios | 'self' |
object-src | Plugins (Flash, Java) | 'none' |
media-src | Audio y video | 'self' |
worker-src | Web Workers y Service Workers | 'self' |
Errores comunes
Estos son los errores que más veo al configurar security headers. Evítalos.
1. CSP demasiado restrictivo que rompe scripts de terceros
El error más frecuente. Configuras una política estricta, haces deploy, y tu analytics, chat widget o login social dejan de funcionar.
La causa: No incluiste los dominios de tus scripts externos en script-src o connect-src.
La solución: Siempre empieza con Content-Security-Policy-Report-Only. Navega tu sitio completo, revisa la consola de DevTools, y agrega cada dominio que aparece como violación. Solo cuando la consola esté limpia, cambia a Content-Security-Policy.
// MAL: directo a producción sin probar
"script-src 'self'"
// BIEN: primero Report-Only, luego agregar dominios necesarios
"script-src 'self' https://www.googletagmanager.com https://va.vercel-scripts.com"2. HSTS sin incluir subdominios
Configuras HSTS en tu dominio principal pero no incluyes includeSubDomains. Un atacante puede hacer downgrade en api.tu-sitio.com o admin.tu-sitio.com.
La solución: Agrega includeSubDomains, pero solo después de verificar que todos tus subdominios soportan HTTPS.
// Incompleto
'Strict-Transport-Security': 'max-age=63072000'
// Correcto
'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload'3. X-Frame-Options demasiado permisivo
Usar SAMEORIGIN cuando deberías usar DENY. Si tu sitio no necesita embeberse en iframes (la mayoría no lo necesita), usa DENY.
// Demasiado permisivo para la mayoría de sitios
'X-Frame-Options': 'SAMEORIGIN'
// Más seguro
'X-Frame-Options': 'DENY'4. No verificar después de configurar
Configuras los headers en next.config.ts, haces deploy y asumes que funcionan. Pero un middleware, un proxy reverso o la configuración de Vercel pueden estar sobreescribiendo tus headers.
La solución: Siempre verifica después del deploy con curl -I o con el verificador de headers de datahogo. Hazlo parte de tu checklist de deploy.
# Después de cada deploy
curl -sI https://tu-sitio.com | grep -iE "strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy"5. Headers solo en el documento HTML
Configuras headers para la ruta /(.*) pero tus API routes o archivos estáticos no los reciben.
La solución: Verifica que el pattern de source en next.config.ts cubra todas las rutas. /(.*) debería cubrir todo, pero si tienes configuraciones más específicas, asegúrate de que no se pisen entre sí.
6. Duplicar headers entre next.config.ts y middleware
Si defines CSP en next.config.ts y también en middleware, ambos se envían y pueden entrar en conflicto. El navegador usa la política más restrictiva, lo que puede romper tu sitio de formas difíciles de debuggear.
La solución: Elige un método y úsalo consistentemente. La estrategia recomendada es headers estáticos en next.config.ts y CSP con nonces en middleware, sin duplicar CSP en ambos lugares.
Preguntas frecuentes
¿Cómo verifico los security headers de mi sitio?
Puedes usar curl -I tu-dominio.com desde la terminal para ver todos los headers de respuesta. También puedes abrir las DevTools del navegador, ir a la pestaña Network, hacer una request y revisar los Response Headers. Si prefieres algo más visual, el verificador de headers de datahogo analiza tu URL y te da un score con recomendaciones específicas para cada header.
¿Qué security headers son obligatorios?
Técnicamente ninguno es obligatorio para que tu sitio funcione, pero los mínimos recomendados son: Strict-Transport-Security (HSTS), X-Content-Type-Options, X-Frame-Options y Referrer-Policy. Content-Security-Policy (CSP) es el más importante pero también el más difícil de configurar correctamente. Sin estos headers, tu sitio queda expuesto a ataques que son completamente prevenibles.
¿Content-Security-Policy puede romper mi sitio?
Sí. CSP es restrictivo por diseño. Si configuras una política muy estricta sin incluir los dominios de tus scripts externos, fuentes o imágenes, el navegador los va a bloquear. Empieza con Content-Security-Policy-Report-Only para detectar violaciones sin bloquear nada, revisa la consola de DevTools, y luego ajusta la política antes de activarla en modo real.
¿Los headers de seguridad afectan el SEO?
Indirectamente sí. Google favorece sitios con HTTPS (que HSTS refuerza). Un sitio vulnerable a XSS o clickjacking puede ser marcado como inseguro por los navegadores, lo que afecta la confianza del usuario y las métricas de engagement que Google considera. Además, un sitio hackeado pierde posiciones rápidamente. Los headers de seguridad son una inversión en la estabilidad de tu posicionamiento.
¿Cómo configuro security headers en Vercel?
En Vercel puedes configurar headers en el archivo vercel.json con la propiedad headers, o directamente en next.config.ts usando la función headers(). La segunda opción es mejor porque te da más control y el código viaja con tu proyecto. Si usas next.config.ts, los headers se aplican automáticamente con cada deploy en Vercel sin configuración adicional.
Verificación final
Después de configurar todos los headers, haz esta verificación rápida:
# Verificar todos los headers de una vez
curl -sI https://tu-sitio.com | grep -iE "strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy|cross-origin-opener|x-xss-protection"El resultado debería mostrar los 8 headers. Si falta alguno, revisa tu configuración.
También puedes agregar un test automatizado a tu CI/CD:
// __tests__/security-headers.test.ts
import { describe, it, expect } from 'vitest'
const REQUIRED_HEADERS = [
'strict-transport-security',
'content-security-policy',
'x-content-type-options',
'x-frame-options',
'referrer-policy',
'permissions-policy',
'cross-origin-opener-policy',
]
describe('Security Headers', () => {
it('debe incluir todos los headers de seguridad', async () => {
const response = await fetch(process.env.SITE_URL!)
for (const header of REQUIRED_HEADERS) {
expect(
response.headers.has(header),
`Falta el header: ${header}`
).toBe(true)
}
})
it('HSTS debe tener max-age de al menos 1 año', async () => {
const response = await fetch(process.env.SITE_URL!)
const hsts = response.headers.get('strict-transport-security') ?? ''
const maxAge = parseInt(hsts.match(/max-age=(\d+)/)?.[1] ?? '0')
expect(maxAge).toBeGreaterThanOrEqual(31536000)
})
it('X-Frame-Options debe ser DENY', async () => {
const response = await fetch(process.env.SITE_URL!)
expect(response.headers.get('x-frame-options')).toBe('DENY')
})
})Los security headers son una de las mejoras de seguridad más rápidas que puedes implementar. Configurarlos toma minutos, no afectan el rendimiento y protegen contra ataques reales. Si tu sitio todavía no los tiene, este es el momento de agregarlos.
Para una guía más profunda sobre cada header individual, revisa el post de headers de seguridad en aplicaciones web. Y si quieres cubrir la seguridad de tu aplicación Next.js de forma integral (XSS, CSRF, SQL Injection, autenticación, rate limiting), la guía de seguridad en aplicaciones Next.js cubre todo con código listo para implementar.
Preguntas frecuentes
¿Cómo verifico los security headers de mi sitio?
Puedes usar curl -I tu-dominio.com desde la terminal para ver todos los headers de respuesta. También puedes abrir las DevTools del navegador, ir a la pestaña Network, hacer una request y revisar los Response Headers. Si prefieres algo más visual, hay herramientas online que analizan tu URL y te dan un score.
¿Qué security headers son obligatorios?
Técnicamente ninguno es obligatorio para que tu sitio funcione, pero los mínimos recomendados son: Strict-Transport-Security (HSTS), X-Content-Type-Options, X-Frame-Options y Referrer-Policy. Content-Security-Policy (CSP) es el más importante pero también el más difícil de configurar correctamente.
¿Content-Security-Policy puede romper mi sitio?
Sí. CSP es restrictivo por diseño. Si configuras una política muy estricta sin incluir los dominios de tus scripts externos, fuentes o imágenes, el navegador los va a bloquear. Empieza con Content-Security-Policy-Report-Only para detectar violaciones sin bloquear nada, y luego ajusta la política.
¿Los headers de seguridad afectan el SEO?
Indirectamente sí. Google favorece sitios con HTTPS (que HSTS refuerza). Además, un sitio vulnerable a XSS o clickjacking puede ser marcado como inseguro por los navegadores, lo que afecta la confianza del usuario y las métricas de engagement que Google considera.
¿Cómo configuro security headers en Vercel?
En Vercel puedes configurar headers en el archivo vercel.json con la propiedad headers, o directamente en next.config.ts usando la función headers(). La segunda opción es mejor porque te da más control y el código viaja con tu proyecto.
Articulos relacionados
Row Level Security en Supabase: Errores Comunes que Dejan tu Base de Datos Abierta
Los 5 errores más comunes de Row Level Security en Supabase que dejan tu base de datos expuesta. USING(true), tablas sin RLS, service_role en el cliente y cómo corregirlos.
OWASP Top 10: Guía Práctica para Desarrolladores Web
Guía práctica del OWASP Top 10 en español. Las 10 vulnerabilidades más críticas en aplicaciones web con ejemplos de código, prevención en Next.js y Node.js, y checklist de seguridad.
Archivos .env Expuestos: Cómo Verificar si tu Sitio Filtra Secretos
Guía para detectar si tu sitio web expone archivos .env, .git y configuraciones sensibles. Verificación manual, protección en Next.js y Vercel, y remediación.