Comprobador de cabeceras de seguridad

Diez cabeceras deciden cuánto te va a costar un bug.

Escribe una URL y leemos las cabeceras de respuesta que tu servidor envía de verdad. Cada una de las de abajo es una sola línea de configuración, y cada una convierte toda una clase de ataque de posible en imposible.

Revisa sitios que sean tuyos o que tengas autorización para probar. Pedimos la página como la pediría un navegador y leemos lo que vuelve. Consulta nuestros términos.

Qué comprueba esto, y cómo es una buena respuesta

Content-Security-Policy

La que más importa, y la que casi ninguna app generada con IA entrega. Le dice al navegador de dónde puede venir el JavaScript. Sin ella, cualquier sitio donde tu app renderiza texto que aportó otra persona, un nombre visible, un comentario, la descripción de un producto traída de una API, se convierte en un sitio donde puede ejecutarse código con acceso total a la sesión. Una buena respuesta empieza en default-src 'self' y se abre a propósito. Una mala falta por completo, o es tan amplia que permite todo lo que se añadió para frenar.

unsafe-eval, comodines y unsafe-inline dentro de esa política

Una política puede existir y aun así ser decoración. 'unsafe-eval' deja que la página convierta cadenas en código ejecutable, que es el mecanismo del que depende la mayoría de las inyecciones. Un * suelto o un https: en las fuentes de script significa que el código puede cargarse desde cualquier host de internet. 'unsafe-inline' significa que el markup inyectado se ejecuta. El matiz que conviene saber: 'strict-dynamic' hace que los navegadores ignoren las fuentes de host, y un nonce o hash hace que ignoren 'unsafe-inline', así que una política que lleva ambos es correcta y no contradictoria. Aplicamos esa regla en lugar de buscar patrones en el texto.

X-Frame-Options y frame-ancestors

Si alguien puede cargar tu app dentro de un frame invisible en su propia página, taparla con sus propios botones y recoger los clics. Al atacante no le cuesta nada y funciona contra cualquier página donde un clic hace algo relevante: confirmar un pago, cambiar un permiso, borrar una cuenta. Cualquiera de las dos cabeceras lo cierra. frame-ancestors es la moderna y puede nombrar orígenes permitidos; X-Frame-Options: DENY es la versión tosca que los navegadores antiguos todavía respetan.

X-Content-Type-Options

Un solo valor, nosniff, y evita que los navegadores adivinen el tipo de un archivo cuando el tipo declarado parece equivocado. Esa adivinanza es como un archivo subido que tu servidor llama documento de texto plano acaba ejecutándose como script. Si tu app acepta subidas y las sirve de vuelta desde tu propio dominio, esta línea no es opcional.

Referrer-Policy

Cuánto de la URL actual viaja con cada clic hacia fuera y cada petición a terceros. El valor por defecto en muchos stacks envía la ruta completa, lo que filtra todo lo que pongas en una URL: tokens de restablecimiento de contraseña, enlaces de invitación, ids de documento, términos de búsqueda. strict-origin-when-cross-origin mantiene la parte útil y descarta el resto.

Permissions-Policy

Qué capacidades del navegador pueden pedir la página y lo que incrusta: cámara, micrófono, geolocalización, pago. Ponerla a denegar lo que no usas significa que un script de terceros comprometido no puede activar ninguna en silencio. La mayoría de apps no necesita ninguna, lo que hace de esta una de las líneas más baratas del archivo.

Divulgación de la versión del servidor

Si tus respuestas anuncian la versión exacta de lo que está corriendo. nginx a secas no le dice a un atacante nada que no pudiera adivinar. nginx/1.18.0 le dice exactamente qué avisos de seguridad debe ir a leer. Esto no es una vulnerabilidad por sí sola, es el paso previo a una.

Access-Control-Allow-Origin

Qué otros sitios pueden leer tus respuestas. Un comodín aquí suele ser alguien resolviendo un error de CORS a las tres de la mañana, y significa que cualquier página de internet puede llamar a tu API y leer lo que vuelve. Merece la pena revisarlo en cada despliegue, porque este es el ajuste con más papeletas de haberse abierto con prisa y no haberse vuelto a cerrar.

Cómo corregirlo

Todas estas se fijan en un solo sitio, y en la mayoría de hosts ese sitio es un único archivo de configuración. La política de abajo es un punto de partida lo bastante estricto para merecer la pena y lo bastante laxo para arrancar una app típica: empieza aquí, mira la consola del navegador y abre solo donde algo que de verdad necesitas esté bloqueado.

Vercel
{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Content-Security-Policy", "value": "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" },
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
        { "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" }
      ]
    }
  ]
}
Netlify
# public/_headers
/*
  Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
  X-Frame-Options: DENY
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=()
Next.js
// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: "/:path*",
        headers: [
          { key: "Content-Security-Policy", value: "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" },
          { key: "X-Frame-Options", value: "DENY" },
          { key: "X-Content-Type-Options", value: "nosniff" },
          { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
          { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
        ],
      },
    ];
  },
};
Cloudflare Worker
export default {
  async fetch(request, env) {
    const response = await env.ASSETS.fetch(request);
    const headers = new Headers(response.headers);
    headers.set("content-security-policy", "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'");
    headers.set("x-frame-options", "DENY");
    headers.set("x-content-type-options", "nosniff");
    headers.set("referrer-policy", "strict-origin-when-cross-origin");
    headers.set("permissions-policy", "camera=(), microphone=(), geolocation=()");
    return new Response(response.body, { status: response.status, headers });
  },
};
Express
import helmet from "helmet";

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        objectSrc: ["'none'"],
        baseUri: ["'self'"],
        frameAncestors: ["'none'"],
      },
    },
    referrerPolicy: { policy: "strict-origin-when-cross-origin" },
  }),
);
Lo que esta herramienta no puede ver

Las cabeceras son el trabajo de seguridad más barato que existe, y por eso mismo arreglarlas demuestra tan poco. Una puntuación perfecta aquí dice que tus respuestas están bien configuradas. No dice nada sobre si la consulta detrás de tu panel comprueba quién está preguntando, sobre si una dependencia que instalaste la primavera pasada tiene un agujero conocido, o sobre si una clave de API lleva seis meses en tu historial de git. Eso vive en el código, y ninguna cabecera lo compensa.

Analizar también el códigoGratis para empezar. Conecta un repositorio y el primer análisis tarda unos dos minutos.