seguridad·14 min de lectura

Headers de Seguridad: Que Son y Como Configurarlos en tu Aplicacion Web

Guia practica 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 linea 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 aplicacion queda expuesta a ataques que son completamente prevenibles.

Lo interesante es que configurarlos toma minutos, pero el nivel de proteccion que agregan es significativo. Vamos header por header, con explicacion de que hace cada uno y codigo para implementarlo.

Por que importan los headers de seguridad

Cuando un navegador carga tu pagina, 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 informacion 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 calificacion de A+ a F y te dice exactamente que te falta.

Content-Security-Policy (CSP)

Content-Security-Policy es el header mas poderoso y el mas 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-srcImagenes'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 basico 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'",
 
  // Imagenes: 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 version anterior usa 'unsafe-inline' y 'unsafe-eval', que debilitan CSP. Para una proteccion real, usa nonces (numeros 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 estaticos 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;

Imagenes externas: Si cargas imagenes 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 conexion 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.

Configuracion

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

Los parametros:

  • max-age=63072000: El navegador recuerda usar HTTPS durante 2 anos (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 validos antes de activar esta directiva.

Empezar gradualmente

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

tsx
// Paso 1: 1 dia 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 anos + 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 podria subir un archivo con extension inocua pero contenido malicioso.

La solucion

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 mas facil 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 pagina 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 ningun caso
{
  key: 'X-Frame-Options',
  value: 'DENY'
}
 
// Permitir iframes solo del mismo origen
{
  key: 'X-Frame-Options',
  value: 'SAMEORIGIN'
}

Relacion con CSP frame-ancestors

X-Frame-Options es el header antiguo. La directiva frame-ancestors de CSP es la version moderna y mas 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 especifico

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

Referrer-Policy

Controla cuanta informacion 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

Recomendacion

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

Este valor es el mas balanceado:

  • Navegacion interna: envia la URL completa (util para analytics)
  • Navegacion a otros sitios HTTPS: envia solo el dominio
  • Navegacion de HTTPS a HTTP: no envia nada

Si manejas datos sensibles en URLs (tokens en query params, que no deberia 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 geolocalizacion, el acelerometro, etc.

Por que 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.

Configuracion

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 ubicacion
    '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 especifico
  • * = permitido para todos

Si tu sitio usa alguna API

tsx
// Ejemplo: sitio que necesita geolocalizacion 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 ubicacion
    'browsing-topics=()',
    'interest-cohort=()',
    'payment=()',
    'usb=()',
  ].join(', ')
}

Implementar todos los headers en NextJS

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

Opcion 1: next.config.ts (recomendado para headers estaticos)

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

Opcion 2: Middleware (para headers dinamicos)

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 metodos para el mismo header

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

Opcion 3: Combinar ambos

La estrategia mas practica es usar next.config.ts para headers estaticos y middleware solo para CSP con nonces:

tsx
// next.config.ts - Headers estaticos
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 automaticamente. Pero tambien 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 recomendacion es mantener los headers en next.config.ts para que sean parte de tu codigo y viajen con el repo. vercel.json es util para headers que son especificos del entorno de produccion. Para mas detalles sobre el deploy en Vercel, revisa la guia 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 implementacion.

1. securityheaders.com

La forma mas rapida. Ve a securityheaders.com, ingresa tu dominio y obtienes una calificacion:

  • 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 deberias 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 algun header no aparece, revisa tu configuracion.

3. DevTools del navegador

  1. Abre DevTools (F12)
  2. Ve a la pestana Network
  3. Recarga la pagina
  4. Click en la primera peticion (el documento HTML)
  5. Revisa la seccion 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 ano', 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 ano en segundos
  })
})

Headers adicionales que vale la pena considerar

Ademas de los principales, hay otros headers que pueden ser utiles dependiendo de tu aplicacion.

X-DNS-Prefetch-Control

Controla si el navegador hace prefetch de DNS para enlaces en tu pagina:

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

Cross-Origin headers

Si tu aplicacion 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 imagenes 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 mas amplia. Si ya los configuraste, el siguiente paso es revisar tu aplicacion 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 codigo esten cubiertas. La guia de seguridad en aplicaciones NextJS cubre XSS, CSRF, SQL Injection, autenticacion y rate limiting en detalle.

Para la parte de secrets y credenciales, datahogo puede escanear tu repositorio de GitHub y detectar API keys, tokens o passwords que hayan quedado expuestos en el codigo. Si encuentra algo, genera un PR con la correccion.

Configuracion completa lista para copiar

Para referencia rapida, aqui esta la configuracion completa de headers de seguridad para un proyecto NextJS tipico:

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 mas rapidas de mejorar la seguridad de tu aplicacion. Configurarlos toma minutos y la proteccion que agregan es real:

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

Empieza con la configuracion basica de next.config.ts, verifica con securityheaders.com que todo este activo, y despues migra CSP a nonces via middleware si necesitas proteccion mas estricta.

La documentacion de MDN sobre headers HTTP y las recomendaciones de OWASP para headers son las referencias definitivas si necesitas profundizar en algun header especifico.

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

Preguntas frecuentes

Que son los headers de seguridad HTTP y por que 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 pagina, si se permite embeber en iframes, si debe forzar HTTPS, y mas. Sin ellos, tu sitio queda expuesto a ataques como XSS, clickjacking e inyeccion de contenido.

Como verifico que mi sitio tiene los headers de seguridad correctos?

Puedes usar securityheaders.com para obtener una calificacion rapida de tus headers. Tambien 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 peticion.

Cual es la diferencia entre configurar headers en next.config.ts y en middleware?

next.config.ts aplica headers estaticos a todas las rutas que coincidan con un patron. Middleware permite headers dinamicos 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 aplicacion NextJS. Que hago?

Es comun 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 conexion, pero sin HSTS un atacante puede interceptar la primera peticion 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.