Saltar al contenido

Cabeceras de seguridad por middleware

El middleware como el lugar canónico para fijar de una vez las cabeceras de seguridad de todo el sitio: HSTS para forzar HTTPS, X-Frame-Options y frame-ancestors contra el clickjacking, Referrer-Policy para no filtrar la URL de origen, Permissions-Policy para desactivar APIs del navegador, y el matiz de que solo alcanzan a las respuestas servidas bajo demanda.

El middleware como lugar de las cabeceras transversales

Una cabecera de seguridad no pertenece a una ruta: pertenece a la respuesta, a cualquiera. Repetirla página por página sería frágil y olvidadizo; el sitio correcto es el borde por el que toda respuesta sale, y ese borde es el middleware. El patrón es el ciclo que ya conoces: dejas que next renderice la ruta, recibes la Response y, en el camino de vuelta, le imprimes las cabeceras antes de devolverla.

TypeScript
// src/middleware.tsimport { defineMiddleware } from 'astro:middleware';export const onRequest = defineMiddleware(async (context, next) => {  const response = await next();  response.headers.set('X-Content-Type-Options', 'nosniff');  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');  response.headers.set('X-Frame-Options', 'DENY');  return response;});

Escribir esto una vez cubre cada ruta que Astro sirva bajo demanda: no hay página que se olvide de su cabecera porque ninguna las pone, las pone el portero por todas. La X-Content-Type-Options: nosniff del ejemplo es una de las más baratas y valiosas: prohíbe al navegador adivinar el tipo de un recurso —el llamado MIME sniffing— y así evita que un archivo servido como texto acabe ejecutándose como script.

Que todas vivan juntas tiene un valor que trasciende la comodidad: una sola pantalla te muestra la postura completa del sitio. Un revisor abre src/middleware.ts y ve, de un vistazo, qué se declara y qué falta —no ha de rastrear diez layouts para reconstruir la política—. En seguridad, poder mirar de una vez lo que protege el sistema no es un lujo estético: es la diferencia entre una postura que alguien puede auditar y una dispersa que nadie llega a abarcar.

HSTS: forzar HTTPS y no volver atrás

Strict-Transport-Security —HSTS— le ordena al navegador que, durante un plazo, hable con tu dominio solo por HTTPS, aunque el usuario teclee http:// o siga un enlace inseguro. Cierra la ventana del ataque man-in-the-middle que actúa en ese primer instante de texto plano antes del salto a cifrado.

TypeScript
response.headers.set(  'Strict-Transport-Security',  'max-age=63072000; includeSubDomains; preload',);

Cada parte pesa. El max-age en segundos es cuánto recordará el navegador la orden —dos años es lo habitual—; includeSubDomains la extiende a todo subdominio; preload te habilita a inscribir el dominio en la lista que los navegadores traen de fábrica, de modo que la primera visita ya llegue forzada. La contrapartida es un compromiso serio: mientras el max-age viva, no hay vuelta a HTTP para ese dominio, así que no actives preload hasta tener HTTPS sólido en el dominio y todos sus subdominios.

El peso de ese compromiso se entiende mejor pensando en la salida. Retirar un dominio de la lista de preload es lento y depende de los ciclos de publicación de cada navegador: no es un cambio que revierta esta tarde. Por eso la recomendación es escalonar —primero un max-age corto para probar que nada se rompe, luego alargarlo, y solo al final, con confianza ganada, includeSubDomains y preload—. HSTS premia la prudencia: es una promesa difícil de deshacer, así que conviene hacerla cuando de verdad puedas cumplirla.

Clickjacking: X-Frame-Options y frame-ancestors

El clickjacking mete tu página en un <iframe> invisible sobre una trampa, para que la víctima crea que pulsa una cosa cuando en realidad acciona la tuya. La defensa es negar que te enmarquen. La cabecera veterana es X-Frame-Options: DENY —o SAMEORIGIN si tú mismo te embebes—, entendida por todos los navegadores.

TypeScript
response.headers.set('X-Frame-Options', 'DENY');response.headers.set(  'Content-Security-Policy',  "frame-ancestors 'none'",);

Su sucesora moderna es la directiva frame-ancestors de la CSP, más expresiva porque admite una lista de orígenes concretos que sí pueden enmarcarte. Lo habitual hoy es poner ambas: X-Frame-Options como red para clientes antiguos y frame-ancestors como la política real. Recuerda el matiz de la lección anterior: frame-ancestors solo surte efecto como cabecera HTTP, nunca dentro de una etiqueta meta, así que este es otro motivo para emitir tu CSP desde el middleware cuando dependes de ella.

Elegir entre DENY y SAMEORIGIN es una decisión de producto: DENY prohíbe todo enmarcado, incluso el tuyo, y es lo correcto si nunca embebes tus propias páginas; SAMEORIGIN lo permite solo desde tu mismo origen, útil si tienes un panel que muestra una vista en un <iframe> propio. Con frame-ancestors la elección se afina —enumeras los orígenes exactos que sí pueden enmarcarte— y por eso es la que manda cuando ambas están presentes. La X-Frame-Options queda como la red por debajo, para el cliente que aún no entienda la directiva moderna.

Referrer-Policy y Permissions-Policy: fugas y superficie

Quedan dos cabeceras que recortan riesgo de formas distintas. Referrer-Policy gobierna cuánta información de dónde venías se envía al navegar a otro sitio. Por defecto, el navegador puede filtrar la URL completa —con sus rutas y parámetros, a veces sensibles— a terceros. Un valor prudente es strict-origin-when-cross-origin: manda la URL entera dentro de tu dominio, pero solo el origen a secas cuando el destino es ajeno, y nada al bajar de HTTPS a HTTP.

TypeScript
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');response.headers.set(  'Permissions-Policy',  'camera=(), microphone=(), geolocation=()',);

Permissions-Policy es la más proactiva: apaga por adelantado APIs potentes del navegador —cámara, micrófono, geolocalización— que tu sitio no usa, reduciendo lo que un script inyectado podría siquiera intentar. La sintaxis camera=() con la lista vacía significa “nadie, ni yo”: si mañana un XSS logra colarse, ya no puede pedir la cámara porque la propia página la declaró prohibida para todos. Es superficie de ataque que eliminas antes de que nadie la busque.

Hay una asimetría útil entre estas dos últimas: Referrer-Policy protege a tus usuarios de filtrar a dónde iban y de dónde venían, mientras que Permissions-Policy te protege a ti recortando lo que un script —tuyo o inyectado— puede siquiera pedirle al navegador. Fíjalas ambas con criterio de mínimo: manda el mínimo de referente que la analítica legítima necesite y enciende el mínimo de APIs que tus funciones reales usen. Todo lo demás, apagado por defecto, es una puerta que ni tú ni un atacante podréis empujar.