Cómo funciona esta comprobación
Scanaris vuelve a pedir tu página principal con una cabecera Origin de un dominio inventado, como haría el navegador si otra web intentara llamarte, y mira qué responde tu servidor. Si Access-Control-Allow-Origin devuelve ese mismo origen y además Access-Control-Allow-Credentials vale true, abre un hallazgo de severidad alta: tu servidor refleja cualquier origen con credenciales. Si responde con el comodín * junto con credenciales, abre uno de severidad baja: los navegadores rechazan esa combinación, pero delata una configuración hecha a ciegas. Solo se prueba la URL escaneada; si tu API vive en otra ruta o dominio, pruébala con el comando de más abajo.
Qué hace un atacante con esto
Monta una página en cualquier dominio con un fetch a tu API usando credentials: 'include'. Cuando uno de tus usuarios, con sesión abierta, visita esa página, el navegador adjunta sus cookies, tu servidor responde con los datos del usuario y, como la política CORS lo permite, la página del atacante los lee. Perfil, pedidos, mensajes, lo que devuelva la API. El usuario no ve nada: ocurre en segundo plano.
Por qué ocurre
CORS existe para que el navegador bloquee por defecto las lecturas entre dominios. Cuando una llamada falla en desarrollo (la app en un puerto, la API en otro), la solución rápida que sugieren los tutoriales y las herramientas de IA es "permitir todos los orígenes", y para que funcionen las cookies se añade "con credenciales". En producción eso significa que la protección quedó desactivada. Más detalle en CORS mal configurado: el fallo silencioso que expone a tus usuarios.
Cómo arreglarlo
- Lista explícita de orígenes. Solo los dominios que de verdad llaman a tu API desde el navegador. En Express:
cors({ origin: ['https://app.tudominio.com'], credentials: true }). En Next.js, en el Route Handler, compara elOrigincon tu lista antes de devolverlo. En Nuxt, conrouteRulesy la opcióncorso un middleware de servidor. - Nunca reflejes el origen. Copiar el valor de
Originen la respuesta "porque así funciona todo" es el error exacto que mide el check. - Si no necesitas cookies entre dominios, quita
Access-Control-Allow-Credentials. Sin credenciales, un*en una API pública de solo lectura es aceptable. - Añade Vary: Origin cuando la respuesta dependa del origen, para que ninguna caché sirva a un dominio la respuesta de otro.
- Cookies con SameSite=Lax o Strict como segunda barrera: aunque CORS fallara, el navegador no las enviaría desde otro sitio.
Comprueba que está arreglado
curl -sI -H "Origin: https://evil.example" https://tudominio.com/api/me | grep -i "access-control"
Lo correcto es que no aparezca Access-Control-Allow-Origin para ese origen, o que aparezca con tu dominio y no con evil.example. Prueba también con tu propio dominio en Origin para confirmar que tu app sigue funcionando.
Preguntas frecuentes
Mi API es pública y sin login. ¿Es un problema?
No, si no hay cookies ni cabeceras de autorización. Access-Control-Allow-Origin: * sin credenciales es la configuración correcta para una API abierta.
Tengo varios subdominios que llaman a la API.
Lístalos todos o valida el Origin contra un patrón estricto de tu dominio raíz. Nunca valides con "contiene tudominio.com": tudominio.com.evil.example lo cumple.
¿Y localhost para desarrollar?
Añádelo solo en el entorno de desarrollo, con una variable de entorno. Un http://localhost:3000 en la lista de producción es un origen que cualquier programa en el ordenador del usuario puede usar.