CORS (Cross-Origin Resource Sharing) es un mecanismo de seguridad del navegador que permite que un frontend en un origen (por ejemplo, https://app.ejemplo.com) pueda leer respuestas de un backend en otro origen (por ejemplo, https://api.ejemplo.com), siempre que el servidor lo autorice explícitamente mediante cabeceras como Access-Control-Allow-Origin. MDN lo describe como un conjunto de cabeceras que amplían el modelo del Same-Origin Policy para permitir accesos controlados entre orígenes.

Punto clave: CORS no es “seguridad del servidor”, es una política del navegador. El servidor decide a quién le da permiso de lectura, y el navegador aplica esa decisión al JavaScript del cliente. OWASP lo resume así: es el cliente el que aplica la restricción según esas cabeceras; por eso una configuración amplia es peligrosa.


1) Qué significa “CORS inseguro con credenciales”

Para que exista un CORS realmente crítico, normalmente coinciden dos condiciones:

  1. El servidor responde con un Access-Control-Allow-Origin demasiado permisivo, por ejemplo:
    • reflejando el Origin que llega en la petición (origin reflection),
    • aceptando null como origen permitido,
    • o usando patrones laxos (“cualquier subdominio que contenga X”).
  2. El servidor permite credenciales con:
    • Access-Control-Allow-Credentials: true

MDN define “credenciales” como cookies, certificados TLS de cliente o cabeceras de autenticación, y advierte que incluir credenciales en peticiones cross-origin puede abrir la puerta a riesgos como CSRF (y, en el caso de CORS permisivo, a lectura de respuestas autenticadas).

Además, MDN indica explícitamente que cuando una respuesta a una petición con credenciales incluye Access-Control-Allow-Origin: *, el navegador bloquea el acceso (no deja leer la respuesta).
Esto genera una falsa sensación de seguridad: “si pongo * con credenciales, el navegador lo bloqueará, así que no pasa nada”. El problema real suele ser otro: los sistemas vulnerables no ponen *; reflejan el origen del atacante.


2) Por qué es crítico: impacto real cuando hay sesión/cookies

Cuando habilitamos CORS con credenciales, estamos permitiendo lo siguiente:

  • Un sitio A (frontend) ejecuta JavaScript en el navegador del usuario.
  • Ese JavaScript solicita un recurso al sitio B (backend/API) incluyendo credenciales del usuario (por ejemplo, cookies de sesión).
  • Si el servidor B responde autorizando ese origen, el navegador permite que el JavaScript de A lea la respuesta.

Si B está mal configurado (acepta cualquier origen o refleja el origen), entonces un atacante puede montar un sitio malicioso y conseguir que el navegador de una víctima autenticada lea datos sensibles de B y los exfiltre.

Esto es, en la práctica, una ruptura del modelo de aislamiento del navegador: PortSwigger lo explica como que confiar orígenes arbitrarios “deshabilita efectivamente” la protección del same-origin, permitiendo interacción bidireccional (leer respuestas) desde terceros.

OWASP, en su guía de testing, destaca específicamente configuraciones inseguras como wildcard y, sobre todo, escenarios donde se refleja Origin o incluso null, especialmente cuando se permiten credenciales, porque eso hace el fallo explotable.

Resumen de impacto (cuando hay autenticación):

  • lectura de endpoints internos “solo para usuario autenticado” (perfil, pedidos, tokens, datos personales),
  • exfiltración de información sin necesidad de XSS en el sitio víctima,
  • en algunos casos, posibilidad de encadenar con acciones sensibles si el backend confía en cookies (aquí ya entran otros controles como CSRF, pero el punto crítico de CORS es la lectura).

3) Casos típicos (y por qué algunos “parecen seguros” pero no lo son)

Caso 1: “No usamos *, usamos el Origin dinámico”

Esto es correcto solo si el Origin dinámico se valida contra una allowlist estricta (coincidencia exacta) y no se refleja indiscriminadamente. PortSwigger y OWASP describen el caso “reflected origin” como condición típica de vulnerabilidad cuando además se permiten credenciales.

Caso 2: “Aceptamos null

MDN advierte que Access-Control-Allow-Origin: null es arriesgado porque “cualquier origen puede crear un documento hostil con origen null” (por ejemplo, iframes sandboxed, archivos locales, etc.), por lo que se recomienda evitarlo.

Caso 3: “Solo son subdominios nuestros”

Esto suele fallar por dos razones:

  • Se valida con endsWith("nuestrodominio.com") sin controlar puntos y límites (p. ej., nuestrodominio.com.evil.tld).
  • Se confía en subdominios que en realidad pueden ser tomados por terceros (subdomain takeover, SaaS abandonado).
    CORS debe asumir que un origen permitido puede convertirse en origen atacante si no tenemos control real de ese dominio.

Caso 4: “Pero el navegador bloquea * con credenciales”

Correcto: el navegador bloquea el acceso a la respuesta cuando hay credenciales y ACAO: *. MDN lo documenta y también lo refleja en el error típico “credentials not supported if ACAO is *”.
Pero lo crítico no es *; lo crítico es permitir credenciales y aceptar orígenes no confiables (incluido “reflejo”).


4) Ejemplos prácticos de configuración vulnerable

Ejemplo A — Origin reflection + credenciales (caso clásico explotable)

Petición (del atacante desde su dominio):

GET /api/account HTTP/1.1
Origin: https://evil.example
Cookie: session=...

Respuesta vulnerable:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://evil.example
Access-Control-Allow-Credentials: true

Esto es exactamente el patrón que PortSwigger y múltiples guías de testing consideran explotable: el servidor acepta el origen del atacante y permite credenciales, por lo que el JavaScript en evil.example puede leer la respuesta del endpoint autenticado.

Ejemplo B — null + credenciales

Respuesta vulnerable:

Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true

OWASP WSTG explica un escenario de explotación usando un iframe sandboxed que fuerza el Origin: null. Si el servidor refleja o permite null con credenciales, estamos ante una vulnerabilidad muy parecida a “permitir cualquiera”.

Ejemplo C — Regex/subcadena mal planteada (bypass por dominio parecido)

Configuraciones del estilo:

  • permitir cualquier origen que “contenga” trusted.com
  • permitir cualquier *.trusted.com sin confirmar control del subdominio

Esto no es un ejemplo “de cabecera”, sino de validación; el resultado es el mismo: orígenes atacantes pasan como permitidos.


5) Cómo verificar la exposición de forma responsable

Recomendamos validar con una batería mínima (sin necesidad de explotación real):

  1. Probar un Origin arbitrario
  • Enviar Origin: https://evil.example y ver si el servidor devuelve Access-Control-Allow-Origin igual a ese valor.
  1. Comprobar si además devuelve Access-Control-Allow-Credentials: true
  • Si ambas cosas están presentes, lo tratamos como crítico en endpoints con datos sensibles.
  1. Probar Origin: null
  • Si responde ACAO: null y permite credenciales, elevamos severidad (OWASP lo marca explícitamente como caso vulnerable).
  1. Verificar el endpoint
  • No todo endpoint es sensible. Pero si el endpoint devuelve datos de cuenta, tokens, configuración, PII o información interna, el riesgo es alto.

6) Cómo cerrarlo sin romper el sitio: corrección por capas

6.1 Principio base: CORS debe ser “mínimo necesario”

  • Si no hay necesidad de cross-origin, no habilitamos CORS.
  • Si hay necesidad, habilitamos solo los orígenes exactos requeridos.

OWASP recomienda buscar configuraciones amplias como wildcard y, por extensión, validar que el servidor no sea permisivo.

6.2 Allowlist estricta + validación exacta del Origin

Implementación conceptual:

  • mantenemos una lista de orígenes permitidos (con esquema + host + puerto cuando aplique),
  • comparamos el Origin recibido contra esa lista por coincidencia exacta,
  • solo entonces devolvemos Access-Control-Allow-Origin: <origin>.

Nunca reflejamos sin validar.

6.3 Credenciales: solo cuando son imprescindibles

Access-Control-Allow-Credentials: true solo debe activarse si:

  • necesitamos cookies/sesión cross-origin, o
  • necesitamos autenticación que el navegador considera “credencial” en este contexto.

MDN recuerda que habilitar credenciales en CORS cambia el riesgo (menciona explícitamente el ángulo CSRF y el hecho de que por defecto no se envían).

Si podemos usar:

  • tokens de corta vida enviados explícitamente (p. ej., en header) sin cookies, o
  • un backend-for-frontend (BFF) en el mismo origen,

reducimos el impacto potencial de un CORS mal configurado.

6.4 Vary: Origin cuando el ACAO varía

Si devolvemos Access-Control-Allow-Origin dinámico (permitiendo varios orígenes concretos), debemos cuidar cachés intermedias:

  • si la respuesta se cachea para un origen y se sirve a otro, podemos crear fugas.
    Buenas prácticas técnicas (y múltiples guías) recomiendan añadir Vary: Origin cuando el ACAO depende del Origin. (En entornos de CORS dinámico, esto es un control operativo esencial).

6.5 Evitar null (salvo caso ultra controlado)

MDN recomienda evitar ACAO: null porque “cualquier origen puede crear un documento hostil con origen null”.
En general, lo tratamos como no permitido.

6.6 Complementos de seguridad: no sustituyen CORS, pero ayudan

  • Si usamos cookies: revisar políticas de cookies (SameSite, Secure, HttpOnly) y CSRF tokens según el flujo.
    CORS mal configurado puede permitir lectura; CSRF protege acciones, pero no reemplaza el control de lectura.
    MDN recalca que el envío de credenciales cross-site tiene implicaciones de seguridad y por defecto está deshabilitado.

7) Errores frecuentes al “arreglar CORS” (y cómo evitarlos)

  1. Cambiar a * pensando que “es más simple”
  • Con credenciales, el navegador lo bloqueará, pero no es una solución real: rompe integraciones y no evita otros fallos de validación.
  1. Permitir null para “arreglar iframes/embeds”
  • Esto amplía demasiado. MDN y OWASP alertan del riesgo.
  1. Validar orígenes con substring/regex laxa
  • Es una causa clásica de bypass (dominios parecidos).
  1. Olvidar Vary: Origin
  • Especialmente crítico si hay CDN o cachés compartidas.
  1. Habilitar CORS en endpoints sensibles “para que funcione el frontend”
  • En vez de diseñar un BFF o ajustar la arquitectura, se abre un agujero que luego es difícil de justificar.

8) Checklist de cierre

  • No existe reflection de Origin sin validación (probado con Origin: https://evil.example).
  • Access-Control-Allow-Credentials: true solo se devuelve en endpoints y rutas que realmente lo necesitan.
  • No se permite Access-Control-Allow-Origin: null (salvo excepción documentada y justificada).
  • Orígenes permitidos definidos por coincidencia exacta (esquema + host + puerto).
  • Se añade Vary: Origin cuando ACAO es dinámico.
  • Se revisa el impacto en caché/CDN y se valida con tráfico real.
  • Se revalida en navegador: no hay CORS errors “por parche”, y tampoco hay permisos excesivos.

9) FAQs

¿Por qué se considera crítico si “solo es una cabecera”?

Porque CORS define quién puede leer respuestas desde JavaScript. Si permitimos credenciales y aceptamos orígenes no confiables, habilitamos exfiltración de respuestas autenticadas. PortSwigger lo describe como una forma de desactivar en la práctica el same-origin cuando se confían orígenes arbitrarios.

¿Access-Control-Allow-Origin: * + Allow-Credentials: true es explotable?

En navegadores modernos, el acceso a la respuesta se bloquea cuando hay credenciales y *. MDN lo explica tanto en la guía general como en el error específico.
Aun así, sigue siendo mala práctica: rompe integraciones y suele indicar que se está “probando a ciegas”. El caso explotable real suele ser reflejo del origin o allowlist mal validada.

¿Cómo lo arreglamos sin romper el frontend?

La receta estable es:

  • allowlist estricta de orígenes,
  • Allow-Credentials solo si es imprescindible,
  • Vary: Origin si ACAO varía,
  • y diseño de arquitectura (BFF o same-site) cuando sea posible.