Use Cases (Actualizado: 2/6/2026)

Security headers web con Claude Code: CSP, nonce, HSTS y despliegue sin romper anuncios

Configura CSP, nonce, HSTS, frame-ancestors y security headers con Claude Code en Next.js, Astro, Express y Cloudflare.

Security headers web con Claude Code: CSP, nonce, HSTS y despliegue sin romper anuncios

Los security headers parecen un detalle de infraestructura, pero cambian por completo la superficie de riesgo de una aplicación web. Con unas pocas cabeceras puedes decirle al navegador qué scripts puede ejecutar, si una pantalla de administración puede aparecer dentro de un iframe, cuánta información de referrer se envía a sitios externos y qué permisos de navegador, como cámara, micrófono o geolocalización, quedan bloqueados por defecto.

Claude Code ayuda mucho en esta tarea porque puede leer el repositorio, encontrar scripts de terceros, editar configuración de framework y generar pruebas. El riesgo es pedirle algo demasiado abierto, por ejemplo: “arregla los errores de CSP”. Una respuesta rápida podría ser script-src * 'unsafe-inline' 'unsafe-eval'. El sitio dejará de mostrar errores, pero también perderá la mayor parte de la protección.

La forma correcta es más disciplinada: inventariar recursos, activar Content-Security-Policy-Report-Only, revisar reportes, ajustar por ruta y recién entonces aplicar la política en modo estricto. Esta guía cubre CSP, nonce, HSTS, X-Frame-Options, frame-ancestors, Referrer-Policy y Permissions-Policy con ejemplos para Next.js, Astro, Express y Cloudflare Pages. También incluye reportes CSP, validación con Security Headers y CSP Evaluator, y choques frecuentes con Google Tag Manager, Google Analytics, AdSense, imágenes y CDNs. Para el marco general de seguridad con Claude Code, revisa también buenas prácticas de seguridad y auditoría de seguridad con Claude Code.

Usa fuentes oficiales mientras adaptes los ejemplos: MDN Content-Security-Policy, Next.js CSP guide, MDN Strict-Transport-Security, hstspreload.org, Cloudflare Pages Headers, Helmet, Google Tag Manager CSP y AdSense con CSP.

Empieza con inventario, no con una cabecera copiada

Antes de cambiar archivos, pide a Claude Code que descubra qué carga realmente el sitio. Esto evita políticas enormes que permiten todo.

Diseña security headers para este repositorio.
Condiciones:
- Primero lista orígenes externos de script, style, image, font, frame y connect.
- CSP debe empezar en modo Report-Only.
- Evita * y unsafe-inline permanente.
- Si Next.js necesita nonce, explica el impacto en rendering dinámico y caché.
- Revisa Google Analytics, GTM, AdSense, CDN de imágenes y YouTube iframe.
- Entrega pasos de verificación con curl, Security Headers y CSP Evaluator.

La tabla inicial debería separar directivas que suelen confundirse. frame-src controla qué iframes puede cargar tu página. frame-ancestors controla quién puede insertar tu página en un iframe. Un fallo de GA4 suele estar en connect-src; una imagen de CDN rota suele estar en img-src; la defensa contra clickjacking vive en frame-ancestors.

HeaderPunto de partidaCuidado en producción
Content-Security-PolicyPrimero Content-Security-Policy-Report-OnlyNo tapes problemas con * o unsafe-inline permanente
Strict-Transport-SecurityEmpieza con max-age=300; includeSubDomainspreload solo si todos los subdominios funcionan con HTTPS
X-Frame-OptionsDENY o SAMEORIGINframe-ancestors es más flexible en navegadores modernos
Referrer-Policystrict-origin-when-cross-originNo pongas tokens ni datos personales en URLs
Permissions-PolicyDesactiva funciones que no usasHabilita pagos, cámara o geolocalización solo si hace falta
X-Content-Type-OptionsnosniffNormalmente conviene en todo el sitio

HSTS preload merece una advertencia aparte. No es una opción para activar el primer día. El sitio de preload recomienda aumentar max-age por etapas y advierte que revertir una inclusión puede tardar. Si un subdominio antiguo, un entorno de pruebas o un dominio de tracking no soporta HTTPS, includeSubDomains; preload puede provocar una interrupción real.

Flujo recomendado de CSP

El flujo sano es observar, clasificar y endurecer.

flowchart LR
  A["Inventario de recursos"] --> B["CSP en Report-Only"]
  B --> C["Reportes y consola del navegador"]
  C --> D["Clasificar anuncios, analytics, CDN, iframes y ruido"]
  D --> E["CSP con nonce o hash"]
  E --> F["Verificar con Security Headers y CSP Evaluator"]

No agregues todos los dominios reportados. Extensiones del navegador, proxies corporativos, scripts viejos y tráfico malicioso pueden generar reportes. Claude Code puede agruparlos, pero la decisión final debe responder a una pregunta simple: ¿este recurso es necesario para el producto?

Next.js con nonce

Next.js es el caso donde más se nota la diferencia entre una política teórica y una política que funciona. La documentación actual de App Router usa proxy.ts para generar un nonce por request. En proyectos antiguos puedes encontrar middleware.ts, pero la idea es igual: crear un valor aleatorio, pasarlo al request header y usarlo en script-src.

Un nonce por request implica rendering dinámico. Una página que necesita un nonce distinto no puede reutilizar siempre el mismo HTML estático. Para blogs públicos y landing pages puede ser mejor usar hashes o mover scripts inline a archivos externos. Para checkout, paneles internos, cuenta de usuario o administración, el nonce suele valer la pena.

// proxy.ts
import { NextRequest, NextResponse } from "next/server";

export function proxy(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
  const isDev = process.env.NODE_ENV !== "production";

  const csp = [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${isDev ? "'unsafe-eval'" : ""} https: http:`,
    `style-src 'self' ${isDev ? "'unsafe-inline'" : `'nonce-${nonce}'`} https://fonts.googleapis.com`,
    "font-src 'self' https://fonts.gstatic.com",
    "img-src 'self' data: blob: https:",
    "connect-src 'self' https://www.google-analytics.com https://analytics.google.com",
    "frame-src 'self' https://www.youtube-nocookie.com",
    "object-src 'none'",
    "base-uri 'self'",
    "form-action 'self'",
    "frame-ancestors 'none'",
    "upgrade-insecure-requests",
    "report-uri /api/csp-report",
  ].join("; ").replace(/\s{2,}/g, " ").trim();

  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);
  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=(), payment=(self)");
  response.headers.set("Strict-Transport-Security", "max-age=300; includeSubDomains");
  return response;
}

export const config = {
  matcher: ["/((?!api/csp-report|_next/static|_next/image|favicon.ico).*)"],
};

Si usas Google Tag Manager, pasa el nonce al snippet o componente. GTM utiliza JavaScript inline para arrancar el contenedor, y Google recomienda el enfoque con nonce. AdSense también documenta CSP estricto porque los dominios de anuncios pueden cambiar con el tiempo. Una allowlist fija puede romper monetización sin aviso.

Endpoint para CSP reports

Report-Only necesita un receptor. En Next.js puedes empezar con este Route Handler:

// app/api/csp-report/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function POST(request: NextRequest) {
  const contentType = request.headers.get("content-type") ?? "";
  const body = await request.text();

  const isReport =
    contentType.includes("application/csp-report") ||
    contentType.includes("application/reports+json") ||
    body.includes("violated-directive");

  if (!isReport) {
    return NextResponse.json({ ok: false }, { status: 415 });
  }

  console.warn("csp-report", body.slice(0, 4000));
  return new NextResponse(null, { status: 204 });
}

En producción no guardes URLs completas si pueden contener datos personales. Guarda ruta, directiva violada, blocked URI, user agent, fecha y contador. report-uri es antiguo, pero sigue siendo práctico por compatibilidad. report-to puede añadirse, pero no lo uses como única vía sin comprobar soporte.

Astro, Express y Cloudflare

En Astro, middleware es un punto cómodo para headers fijos. En sitios estáticos, reducir JavaScript inline suele ser más simple que forzar nonces.

// src/middleware.ts
import { defineMiddleware } from "astro:middleware";

const securityHeaders: Record<string, string> = {
  "Content-Security-Policy-Report-Only": "default-src 'self'; script-src 'self' https://www.googletagmanager.com; img-src 'self' data: blob: https:; connect-src 'self' https://www.google-analytics.com; frame-src 'self' https://www.youtube-nocookie.com; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; report-uri /api/csp-report",
  "X-Content-Type-Options": "nosniff",
  "X-Frame-Options": "DENY",
  "Referrer-Policy": "strict-origin-when-cross-origin",
  "Permissions-Policy": "camera=(), microphone=(), geolocation=(), payment=(self)",
};

export const onRequest = defineMiddleware(async (_context, next) => {
  const response = await next();
  for (const [name, value] of Object.entries(securityHeaders)) {
    response.headers.set(name, value);
  }
  return response;
});

En Express, Helmet es el camino práctico, pero CSP necesita configuración por aplicación.

import crypto from "node:crypto";
import express from "express";
import helmet from "helmet";

const app = express();

app.use((req, res, next) => {
  res.locals.cspNonce = crypto.randomBytes(16).toString("base64");
  next();
});

app.use(helmet({
  contentSecurityPolicy: {
    useDefaults: false,
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`, "'strict-dynamic'", "https:", "http:"],
      imgSrc: ["'self'", "data:", "blob:", "https:"],
      connectSrc: ["'self'", "https://www.google-analytics.com"],
      objectSrc: ["'none'"],
      baseUri: ["'self'"],
      frameAncestors: ["'none'"],
      reportUri: ["/csp-report"],
    },
  },
  strictTransportSecurity: { maxAge: 300, includeSubDomains: true },
  referrerPolicy: { policy: "strict-origin-when-cross-origin" },
  xFrameOptions: { action: "deny" },
}));

Cloudflare Pages usa _headers para cabeceras estáticas. Ese archivo no genera nonces por request.

/*
  X-Content-Type-Options: nosniff
  X-Frame-Options: DENY
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(self)
  Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://www.googletagmanager.com; img-src 'self' data: blob: https:; connect-src 'self' https://www.google-analytics.com; frame-src 'self' https://www.youtube-nocookie.com; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; report-uri /csp-report

Casos de uso y errores habituales

Un sitio de contenido con AdSense, GA4, GTM, fuentes externas e imágenes de CDN debe protegerse sin romper ingresos. Empieza en Report-Only, revisa si los anuncios se muestran, si los eventos de Analytics llegan y si las imágenes siguen cargando. No persigas una nota perfecta si eso deja sin monetización una página clave.

Un SaaS con panel privado tiene otra prioridad: menos terceros y políticas más estrictas. En rutas de administración conviene frame-ancestors 'none', object-src 'none', base-uri 'self' y form-action limitado. Un SDK de pago o chat no necesita permiso global si solo vive en una ruta.

Un widget embebible exige una política distinta. Si debe vivir dentro de iframes de clientes, no puede tener X-Frame-Options: DENY en esa ruta. Usa headers por ruta y define dominios permitidos en frame-ancestors.

Verificación y resultado probado

Verifica al menos tres URLs: home, login o formulario, y una ruta de embed o checkout.

curl -I https://example.com/
curl -I https://example.com/login
curl -I https://example.com/embed/widget

Después usa Security Headers para revisar el conjunto y CSP Evaluator para debilidades de CSP. La nota es una señal, no el objetivo. El objetivo real es que el sitio quede más protegido sin romper anuncios, métricas, pagos ni iframes legítimos.

En la prueba usada para este artículo, Report-Only fue la decisión más valiosa. Separó problemas de nonce en GTM, connect-src en GA4, frame-src en YouTube e img-src en CDN. Con esos reportes, Claude Code pudo proponer políticas por ruta, pasar nonce donde hacía falta y eliminar tags innecesarios. Si quieres aplicar este proceso sobre un repositorio real, la página de formación y consultoría de Claude Code y los productos y plantillas son el siguiente paso práctico.

#Claude Code #security #HTTP headers #CSP #web development
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.