Saltar a contenido

ADR-0007 · CSP con nonce y cabeceras de seguridad en el storefront

  • Estado: aceptado
  • Decisores: Infraestructura / TI
  • Fecha de la decisión: 2026-09-02 (aplicada en staging como ADR-0006; incorporada a develop el 2026-09-10 como ADR-0007 porque el número 0006 quedó usado por la actualización a Next 16)

Contexto y problema

El storefront no emitía ninguna cabecera de seguridad: sin CSP, sin control de framing, sin Referrer-Policy ni Permissions-Policy, y exponiendo X-Powered-By: Next.js. Una CSP protege contra inyección de scripts (XSS), que es el riesgo principal de un frontend de comercio.

La decisión de plataforma (ADR-0001 del repo de infraestructura) ya definió el reparto: la CSP la genera el storefront (es quien conoce sus propios scripts) y las cabeceras fijas de zona (sobre todo HSTS) van en Cloudflare. Este ADR registra el cómo dentro del storefront.

Opciones consideradas

  1. CSP con nonce en el middleware, arrancando en Report-Only (elegida).
  2. CSP estática en next.config.js con unsafe-inline: más simple, pero deja pasar scripts inline (debilita justo lo que la CSP debe frenar) y no usa nonce.
  3. No poner CSP hasta tener un servidor web dedicado: se descartó — la CSP no depende de eso (ver ADR-0001) y el sitio queda sin protección mientras tanto.

Decisión

  • CSP con nonce por petición, generada en src/proxy.ts (el proxy (antes middleware) que ya resuelve el routing por país). El nonce se pasa a Next.js vía el header del request para que sus <script> lo lleven; los estilos usan unsafe-inline (bajo riesgo).
  • Arranca en modo Content-Security-Policy-Report-Only: el navegador no bloquea, solo reporta. Se despliega a staging, se recorren los flujos (búsqueda, ficha, checkout con reCAPTCHA y Bancard) observando la consola, se ajustan los orígenes faltantes y recién con la consola limpia se pasa a enforcing (Content-Security-Policy). Es la red de seguridad para meter CSP en un sitio que ya funciona.
  • El modo es una variable de runtime, CSP_ENFORCE (2026-09-21): true emite la política bloqueante, cualquier otro valor o su ausencia deja el modo reporte. La lee el proxy al arrancar, no se inlinea en el build, así que un ambiente se puede pasar a bloqueante —y volver atrás si algo se rompe— cambiando la variable y reiniciando, sin reconstruir ni desplegar. Eso es lo que hace barato el intento: el paso a enforcing deja de ser un cambio de código.
  • Orígenes de terceros contemplados: reCAPTCHA (google.com/gstatic.com), Algolia (*.algolia.net/*.algolianet.com), backend de Medusa (SDK y SSE de Bancard, leído de NEXT_PUBLIC_MEDUSA_BASE_URL para adaptarse por ambiente), Stripe, e imágenes (S3 de Medusa, imagedelivery.net, www.compulandia.com.py).
  • Cabeceras adicionales emitidas en el mismo middleware: Referrer-Policy, X-Content-Type-Options: nosniff, Permissions-Policy (cámara, micrófono, geolocalización y Topics deshabilitados — el storefront no usa esas APIs) y frame-ancestors 'none' (anti-clickjacking). En next.config.js, poweredByHeader: false oculta X-Powered-By.
  • HSTS queda en Cloudflare (no en el storefront), por ser de zona y requerir activación gradual (ver ADR-0001 y la historia #470).

Consecuencias

Positivas

  • El storefront pasa de cero cabeceras a una CSP con nonce más el resto de las cabeceras.
  • Report-Only permite llegar a enforcing sin romper pagos, búsqueda ni checkout.
  • La CSP se adapta sola al backend de cada ambiente (lee la variable de entorno).

Negativas / deuda asumida

  • La CSP vive dentro de src/proxy.ts (el middleware de routing, renombrado por Next 16): hay que tener cuidado al tocar ese archivo.
  • Queda pendiente el ciclo de validación en staging y el paso a enforcing (hoy, activar CSP_ENFORCE=true en el ambiente).
  • style-src 'unsafe-inline' se mantiene por Next/Tailwind; endurecerlo requeriría nonce/hash en estilos, fuera de alcance por ahora.

Nota 2026-09-10

Al portar la decisión a develop se agregó el origen de Bancard vPOS (NEXT_PUBLIC_BANCARD_VPOS_ORIGIN) en frame-src y connect-src, por variable de entorno para no llevar el puerto 8888 de staging a producción. El script de checkout de Bancard se aloja en public/vendor/bancard/ y se carga desde el propio origen con nonce (historia #149, tarea #160).

Nota 2026-09-21

Relevamiento local del HTML servido (home, tienda, carrito, cuenta) con todo el trabajo de marca, menús jerárquicos y analítica ya integrado: todos los <script> llevan nonce y los únicos orígenes externos que aparecen son imagedelivery.net y www.compulandia.com.py en imágenes, ambos ya permitidos. La tipografía Plus Jakarta Sans se sirve desde el repo con next/font/local, así que font-src 'self' alcanza. Las redes sociales y WhatsApp son enlaces, no recursos: la CSP no los toca.

Queda un punto abierto que solo se puede ver en staging: Cloudflare Zaraz inyecta su script en el HTML después de que la respuesta sale de Next, así que esa etiqueta no lleva nuestro nonce. Con 'strict-dynamic' los hosts de script-src se ignoran, de modo que un script sin nonce queda bloqueado por más que se agregue su dominio. Antes de activar CSP_ENFORCE hay que comprobar en staging si Zaraz sobrevive y, si no, resolverlo del lado de Cloudflare.