seguridad·14 min de lectura

Headers de Seguridad: Que Son y Como Configurarlos en tu Aplicación Web

guía práctica sobre headers de seguridad HTTP. CSP, HSTS, X-Frame-Options, Referrer-Policy y como implementarlos en NextJS con next.config.ts y middleware.

Headers de Seguridad: Que Son y Como Configurarlos

Los headers de seguridad en tu app web son la primera línea de defensa entre tu servidor y el navegador del usuario. Son instrucciones HTTP que le dicen al navegador que puede y que no puede hacer cuando carga tu sitio. Sin ellos, tu aplicación queda expuesta a ataques que son completamente prevenibles.

Lo interesante es que configurarlos toma minutos, pero el nivel de protección que agregan es significativo. Vamos header por header, con explicación de qué hace cada uno y código para implementarlo.

por qué importan los headers de seguridad

Cuando un navegador carga tu página, hace exactamente lo que le dices. Si no le dices nada sobre seguridad, asume que todo esta permitido: cargar scripts de cualquier dominio, embeber tu sitio en un iframe, enviar información del referrer a terceros.

Los headers de seguridad cambian ese comportamiento por defecto. Le dicen al navegador:

  • "Solo carga scripts de mi dominio" (CSP)
  • "Siempre usa HTTPS" (HSTS)
  • "No dejes que otro sitio me meta en un iframe" (X-Frame-Options)
  • "No adivines el tipo de archivo, usa el que yo te digo" (X-Content-Type-Options)

Puedes verificar el estado actual de los headers de tu sitio en securityheaders.com. Te da una calificación de A+ a F y te dice exactamente que te falta.

Content-Security-Policy (CSP)

Content-Security-Policy es el header más poderoso y el más complejo. Define una whitelist de fuentes desde donde el navegador puede cargar recursos.

Que controla CSP

DirectivaControlaEjemplo
default-srcFuente por defecto para todo'self'
script-srcScripts JavaScript'self' https://cdn.ejemplo.com
style-srcHojas de estilo CSS'self' 'unsafe-inline'
img-srcimágenes'self' data: https:
font-srcFuentes tipograficas'self' https://fonts.gstatic.com
connect-srcFetch, XHR, WebSocket'self' https://api.ejemplo.com
frame-srcIframes'none'
frame-ancestorsQuien puede embeber tu sitio'none'
base-uriTags base'self'
form-actionDestinos de formularios'self'
object-srcPlugins (Flash, Java)'none'

CSP básico para NextJS

tsx
// next.config.ts
import type { NextConfig } from 'next'
 
const ContentSecurityPolicy = [
  // Por defecto, solo cargar recursos del mismo origen
  "default-src 'self'",
 
  // Scripts: self + inline necesario para NextJS
  "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
 
  // Estilos: self + inline para Tailwind/CSS-in-JS
  "style-src 'self' 'unsafe-inline'",
 
  // imágenes: self + data URIs + cualquier HTTPS
  "img-src 'self' data: https:",
 
  // Fuentes: self + Google Fonts
  "font-src 'self' https://fonts.gstatic.com",
 
  // Conexiones: self + tu API
  "connect-src 'self' https://api.tudominio.com",
 
  // No permitir iframes dentro de tu sitio
  "frame-src 'none'",
 
  // No permitir que otros sitios te pongan en un iframe
  "frame-ancestors 'none'",
 
  // Restringir base URI
  "base-uri 'self'",
 
  // Restringir destinos de formularios
  "form-action 'self'",
 
  // No permitir plugins (Flash, Java, etc.)
  "object-src 'none'",
].join('; ')
 
const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: ContentSecurityPolicy,
          },
        ],
      },
    ]
  },
}
 
export default nextConfig

CSP estricto con nonces

La versión anterior usa 'unsafe-inline' y 'unsafe-eval', que debilitan CSP. Para una protección real, usa nonces (números aleatorios de un solo uso):

tsx
// 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'",
    "connect-src 'self'",
    "frame-src 'none'",
    "frame-ancestors 'none'",
    "base-uri 'self'",
    "form-action 'self'",
    "object-src 'none'",
  ].join('; ')
 
  // Pasar el nonce a Server Components via header
  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: [
    // Excluir archivos estáticos y API de Next
    {
      source: '/((?!_next/static|_next/image|favicon.ico).*)',
      missing: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
  ],
}
tsx
// 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}
        {/* Todos los scripts necesitan el nonce */}
        <Script
          src="https://analytics.ejemplo.com/script.js"
          nonce={nonce}
          strategy="afterInteractive"
        />
      </body>
    </html>
  )
}
Empieza en modo reporte

Si no sabes que va a romper CSP en tu app, usa primero Content-Security-Policy-Report-Only en lugar de Content-Security-Policy. Esto loguea las violaciones en la consola del navegador sin bloquear nada. Ajusta las directivas y cuando todo este limpio, cambia al header real.

Errores comunes con CSP

Google Fonts: Si usas Google Fonts, necesitas permitir los dominios:

plaintext
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;

Analytics de terceros: Si usas Google Analytics, Vercel Analytics, etc.:

plaintext
script-src 'self' https://www.googletagmanager.com https://va.vercel-scripts.com;
connect-src 'self' https://www.google-analytics.com https://vitals.vercel-insights.com;

imágenes externas: Si cargas imágenes de CDNs o S3:

plaintext
img-src 'self' data: https://tu-bucket.s3.amazonaws.com https://res.cloudinary.com;

Strict-Transport-Security (HSTS)

HSTS le dice al navegador que siempre use HTTPS para conectarse a tu sitio, incluso si el usuario escribe http:// en la barra de direcciones.

El problema que resuelve

Sin HSTS, esto pasa:

  1. El usuario escribe http://tudominio.com
  2. Tu servidor redirige a https://tudominio.com
  3. Pero entre el paso 1 y 2, la conexión no esta encriptada
  4. Un atacante en la misma red puede interceptar ese request HTTP inicial

Con HSTS, el navegador recuerda que tu sitio usa HTTPS y nunca hace el request HTTP inicial.

Configuración

tsx
// next.config.ts (dentro de headers)
{
  key: 'Strict-Transport-Security',
  value: 'max-age=63072000; includeSubDomains; preload'
}

Los parámetros:

  • max-age=63072000: El navegador recuerda usar HTTPS durante 2 años (en segundos)
  • includeSubDomains: Aplica a todos los subdominios
  • preload: Permite incluir tu dominio en la lista de precarga de HSTS de los navegadores
Cuidado con includeSubDomains

Si tienes subdominios que no soportan HTTPS (por ejemplo, un servidor de desarrollo interno), includeSubDomains los va a romper. Verifica que todos tus subdominios tengan certificados SSL válidos antes de activar esta directiva.

Empezar gradualmente

Si no estas seguro, empieza con un max-age corto:

tsx
// Paso 1: 1 día para probar
'Strict-Transport-Security': 'max-age=86400'
 
// Paso 2: 1 semana si todo funciona
'Strict-Transport-Security': 'max-age=604800; includeSubDomains'
 
// Paso 3: 2 años + preload
'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload'

X-Content-Type-Options

Este header previene que el navegador "adivine" el tipo de contenido de un archivo (MIME sniffing).

El problema

Sin este header, si subes un archivo .txt con contenido JavaScript, el navegador puede interpretarlo como script y ejecutarlo. Un atacante podría subir un archivo con extension inocua pero contenido malicioso.

La solución

tsx
// next.config.ts (dentro de headers)
{
  key: 'X-Content-Type-Options',
  value: 'nosniff'
}

Solo tiene un valor posible: nosniff. Le dice al navegador: "usa el Content-Type que yo te digo, no intentes adivinar".

Es el header más fácil de implementar y no tiene efectos secundarios si tu servidor ya envia Content-Types correctos. No hay razon para no tenerlo.

X-Frame-Options

Previene que otros sitios web pongan tu página dentro de un iframe. Esto protege contra ataques de clickjacking, donde un atacante pone tu sitio invisible sobre otro contenido para que el usuario haga click sin saberlo.

Valores posibles

tsx
// No permitir iframes en ningún caso
{
  key: 'X-Frame-Options',
  value: 'DENY'
}
 
// Permitir iframes solo del mismo origen
{
  key: 'X-Frame-Options',
  value: 'SAMEORIGIN'
}

Relación con CSP frame-ancestors

X-Frame-Options es el header antiguo. La directiva frame-ancestors de CSP es la versión moderna y más flexible:

plaintext
Content-Security-Policy: frame-ancestors 'none';           // Equivalente a DENY
Content-Security-Policy: frame-ancestors 'self';           // Equivalente a SAMEORIGIN
Content-Security-Policy: frame-ancestors https://permitido.com; // Permitir un dominio específico

Usa ambos para compatibilidad con navegadores antiguos. Si hay conflicto, CSP tiene prioridad en navegadores modernos.

Referrer-Policy

Controla cuanta información se envia en el header Referer cuando un usuario navega desde tu sitio a otro. Por defecto, el navegador envia la URL completa, que puede incluir datos sensibles en query params.

Valores comunes

ValorQue enviaEjemplo
no-referrerNada(vacio)
originSolo el dominiohttps://tudominio.com
strict-originSolo dominio, solo HTTPS a HTTPShttps://tudominio.com
origin-when-cross-originURL completa al mismo sitio, solo dominio a otrosURL completa o solo dominio
strict-origin-when-cross-originSimilar pero respeta HTTPSRecomendado
no-referrer-when-downgradeNo envia de HTTPS a HTTPDefault del navegador

Recomendación

tsx
// next.config.ts (dentro de headers)
{
  key: 'Referrer-Policy',
  value: 'strict-origin-when-cross-origin'
}

Este valor es el más balanceado:

  • Navegación interna: envia la URL completa (útil para analytics)
  • Navegación a otros sitios HTTPS: envia solo el dominio
  • Navegación de HTTPS a HTTP: no envia nada

Si manejas datos sensibles en URLs (tokens en query params, que no debería pasar pero pasa), usa strict-origin o no-referrer.

Permissions-Policy

Controla que APIs del navegador puede usar tu sitio. Cosas como la camara, el microfono, la geolocalización, el acelerometro, etc.

por qué importa

Si tu sitio no usa la camara, no hay razon para que un script inyectado pueda acceder a ella. Permissions-Policy te permite desactivar APIs que no necesitas.

Configuración

tsx
// next.config.ts (dentro de headers)
{
  key: 'Permissions-Policy',
  value: [
    'camera=()',           // Nadie puede usar la camara
    'microphone=()',       // Nadie puede usar el microfono
    'geolocation=()',      // Nadie puede acceder a la ubicación
    'browsing-topics=()',  // Desactivar Topics API (tracking de Google)
    'interest-cohort=()',  // Desactivar FLoC (tracking de Google)
    'payment=(self)',      // Solo tu sitio puede usar Payment API
    'usb=()',              // Desactivar acceso USB
    'bluetooth=()',        // Desactivar acceso Bluetooth
    'accelerometer=()',    // Desactivar acelerometro
    'gyroscope=()',        // Desactivar giroscopio
  ].join(', ')
}

El formato es feature=(allowlist) donde:

  • () = desactivado para todos
  • (self) = solo tu dominio
  • (self "https://permitido.com") = tu dominio y un dominio específico
  • * = permitido para todos

Si tu sitio usa alguna API

tsx
// Ejemplo: sitio que necesita geolocalización y camara
{
  key: 'Permissions-Policy',
  value: [
    'camera=(self)',              // Solo tu sitio puede usar la camara
    'microphone=()',              // Nadie usa el microfono
    'geolocation=(self)',         // Solo tu sitio puede pedir ubicación
    'browsing-topics=()',
    'interest-cohort=()',
    'payment=()',
    'usb=()',
  ].join(', ')
}

Implementar todos los headers en NextJS

Ahora que conoces cada header, vamos a implementarlos todos juntos.

Opción 1: next.config.ts (recomendado para headers estáticos)

tsx
// next.config.ts
import type { NextConfig } from 'next'
 
const securityHeaders = [
  {
    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: '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=()',
    ].join(', '),
  },
]
 
const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: securityHeaders,
      },
    ]
  },
}
 
export default nextConfig

Opción 2: Middleware (para headers dinámicos)

Si necesitas nonces para CSP o headers que cambien por request:

tsx
// 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'",
    "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 },
  })
 
  // Headers de seguridad
  response.headers.set('Content-Security-Policy', csp)
  response.headers.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload')
  response.headers.set('X-Content-Type-Options', 'nosniff')
  response.headers.set('X-Frame-Options', 'DENY')
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
  response.headers.set(
    'Permissions-Policy',
    'camera=(), microphone=(), geolocation=(), browsing-topics=()'
  )
 
  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' },
      ],
    },
  ],
}
No mezcles ambos métodos para el mismo header

Si defines un header tanto en next.config.ts como en middleware, el de middleware tiene prioridad. Elige un método y usalo consistentemente para evitar confusiones. Si necesitas nonces, usa middleware para CSP y next.config.ts para el resto.

Opción 3: Combinar ambos

La estrategia más práctica es usar next.config.ts para headers estáticos y middleware solo para CSP con nonces:

tsx
// next.config.ts - Headers estáticos
const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          // CSP se maneja en middleware por los nonces
          {
            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=()',
          },
        ],
      },
    ]
  },
}
tsx
// middleware.ts - Solo CSP con nonce
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'",
    "connect-src 'self'",
    "frame-src 'none'",
    "frame-ancestors 'none'",
    "base-uri 'self'",
    "form-action 'self'",
    "object-src 'none'",
  ].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
}

Headers en Vercel

Si tu proyecto esta en Vercel (como la mayoria de proyectos NextJS), los headers de next.config.ts se aplican automáticamente. Pero también puedes definirlos en vercel.json:

json
{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "X-Content-Type-Options",
          "value": "nosniff"
        },
        {
          "key": "X-Frame-Options",
          "value": "DENY"
        },
        {
          "key": "Referrer-Policy",
          "value": "strict-origin-when-cross-origin"
        }
      ]
    }
  ]
}

La recomendación es mantener los headers en next.config.ts para que sean parte de tu código y viajen con el repo. vercel.json es útil para headers que son especificos del entorno de producción. Para más detalles sobre el deploy en Vercel, revisa la guía de deploy de NextJS en Vercel.

Verificar tus headers

Configurar headers sin verificar que estan activos es como escribir tests sin correrlos. Estas son las formas de comprobar tu implementación.

1. securityheaders.com

La forma más rápida. Ve a securityheaders.com, ingresa tu dominio y obtienes una calificación:

  • A+: Todos los headers recomendados presentes y correctamente configurados
  • A: La mayoria de headers presentes
  • B-F: Faltan headers importantes

El reporte te dice exactamente que headers faltan y que valores deberías usar.

2. curl desde la terminal

bash
curl -I https://tudominio.com

Esto muestra los headers de respuesta. Busca los que configuraste:

bash
HTTP/2 200
content-type: text/html; charset=utf-8
content-security-policy: default-src 'self'; script-src 'self' ...
strict-transport-security: max-age=63072000; includeSubDomains; preload
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()

Si algún header no aparece, revisa tu configuración.

3. DevTools del navegador

  1. Abre DevTools (F12)
  2. Ve a la pestana Network
  3. Recarga la página
  4. Click en la primera petición (el documento HTML)
  5. Revisa la sección Response Headers

4. Test automatizado

Puedes crear un test que verifique los headers en tu CI/CD:

tsx
// __tests__/security-headers.test.ts
import { describe, it, expect } from 'vitest'
 
const HEADERS_REQUERIDOS = [
  'content-security-policy',
  'strict-transport-security',
  'x-content-type-options',
  'x-frame-options',
  'referrer-policy',
  'permissions-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 HEADERS_REQUERIDOS) {
      expect(
        response.headers.has(header),
        `Falta el header: ${header}`
      ).toBe(true)
    }
  })
 
  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')
  })
 
  it('X-Content-Type-Options debe ser nosniff', async () => {
    const response = await fetch(process.env.SITE_URL!)
    expect(response.headers.get('x-content-type-options')).toBe('nosniff')
  })
 
  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) // 1 año en segundos
  })
})

Headers adicionales que vale la pena considerar

además de los principales, hay otros headers que pueden ser útiles dependiendo de tu aplicación.

X-DNS-Prefetch-Control

Controla si el navegador hace prefetch de DNS para enlaces en tu página:

tsx
{
  key: 'X-DNS-Prefetch-Control',
  value: 'on' // 'on' para mejor performance, 'off' para más privacidad
}

Cross-Origin headers

Si tu aplicación carga recursos cross-origin (fuentes, scripts de CDN):

tsx
// Previene que otros sitios lean tus recursos via fetch
{
  key: 'Cross-Origin-Resource-Policy',
  value: 'same-origin'
}
 
// Aisla tu sitio para prevenir ataques de timing
{
  key: 'Cross-Origin-Opener-Policy',
  value: 'same-origin'
}
 
// Necesario para SharedArrayBuffer y APIs de alta precision
{
  key: 'Cross-Origin-Embedder-Policy',
  value: 'require-corp'
}
Cross-Origin-Embedder-Policy puede romper cosas

require-corp bloquea la carga de cualquier recurso cross-origin que no incluya el header Cross-Origin-Resource-Policy. Si cargas imágenes de CDNs externos, fuentes de Google, o scripts de terceros, este header puede romper tu sitio. Usalo solo si controlas todos los recursos.

Postura de seguridad completa

Los headers de seguridad son una capa importante, pero son parte de una estrategia más amplia. Si ya los configuraste, el siguiente paso es revisar tu aplicación de forma integral.

Esto incluye verificar que tu repositorio no tenga secrets expuestos, que las variables de entorno esten bien configuradas y que las vulnerabilidades a nivel de código esten cubiertas. La guía de seguridad en aplicaciones NextJS cubre XSS, CSRF, SQL Injection, autenticación y rate limiting en detalle.

Verifica los headers de tu sitio

Verificador de headers de seguridad gratuito -- Pega tu URL y te muestra qué headers tienes, cuáles faltan y qué valor deberían tener.

Configuración completa lista para copiar

Para referencia rápida, aquí esta la configuración completa de headers de seguridad para un proyecto NextJS típico:

tsx
// next.config.ts
import type { NextConfig } from 'next'
 
const securityHeaders = [
  {
    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-DNS-Prefetch-Control',
    value: 'on',
  },
]
 
const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: securityHeaders,
      },
    ]
  },
}
 
export default nextConfig
tsx
// middleware.ts - CSP con nonces
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'",
    "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' },
      ],
    },
  ],
}

Resumen

Los headers de seguridad HTTP son una de las formas más rapidas de mejorar la seguridad de tu aplicación. Configurarlos toma minutos y la protección que agregan es real:

HeaderProtege contraPrioridad
Content-Security-PolicyXSS, inyección de contenidoAlta
Strict-Transport-SecurityAtaques de downgrade HTTPSAlta
X-Content-Type-OptionsMIME sniffingAlta
X-Frame-OptionsClickjackingMedia
Referrer-PolicyFuga de información en URLsMedia
Permissions-PolicyAcceso no autorizado a APIs del navegadorMedia

Empieza con la configuración básica de next.config.ts, verifica con securityheaders.com que todo este activo, y después migra CSP a nonces via middleware si necesitas protección más estricta.

La documentación de MDN sobre headers HTTP y las recomendaciones de OWASP para headers son las referencias definitivas si necesitas profundizar en algún header específico.

#seguridad#headers#csp#hsts#nextjs#http

Preguntas frecuentes

¿Qué son los headers de seguridad HTTP y por qué son importantes?

Los headers de seguridad HTTP son instrucciones que tu servidor envia al navegador indicandole como debe comportarse al cargar tu sitio. Controlan que recursos puede cargar la página, si se permite embeber en iframes, si debe forzar HTTPS, y más. Sin ellos, tu sitio queda expuesto a ataques como XSS, clickjacking e inyección de contenido.

¿Cómo verifico que mi sitio tiene los headers de seguridad correctos?

Puedes usar securityheaders.com para obtener una calificación rápida de tus headers. también puedes usar curl -I tudominio.com desde la terminal o las DevTools del navegador en la pestana Network para ver los headers de respuesta de cualquier petición.

¿Cuál es la diferencia entre configurar headers en next.config.ts y en middleware?

next.config.ts aplica headers estáticos a todas las rutas que coincidan con un patron. Middleware permite headers dinámicos que pueden cambiar por request, como nonces para CSP. Usa next.config.ts para headers fijos y middleware cuando necesites valores generados por request.

¿Content-Security-Policy rompe mi aplicación NextJS. Que hago?

Es común que CSP estricto rompa funcionalidad. Empieza con Content-Security-Policy-Report-Only para ver que se bloquea sin afectar usuarios. Revisa la consola del navegador para ver las violaciones, ajusta las directivas una por una, y cuando todo funcione cambia a Content-Security-Policy.

¿Necesito configurar HSTS si ya tengo HTTPS?

Si. HTTPS encripta la conexión, pero sin HSTS un atacante puede interceptar la primera petición HTTP (antes del redirect a HTTPS) y hacer un ataque de downgrade. HSTS le dice al navegador que siempre use HTTPS directamente, eliminando esa ventana de vulnerabilidad.