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
stagingcomo ADR-0006; incorporada adevelopel 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¶
- CSP con nonce en el middleware, arrancando en Report-Only (elegida).
- CSP estática en
next.config.jsconunsafe-inline: más simple, pero deja pasar scripts inline (debilita justo lo que la CSP debe frenar) y no usa nonce. - 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 usanunsafe-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):trueemite 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 deNEXT_PUBLIC_MEDUSA_BASE_URLpara 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) yframe-ancestors 'none'(anti-clickjacking). Ennext.config.js,poweredByHeader: falseocultaX-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=trueen 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.