ofensiva
CORS
Same-Origin Policy
seguridad web

CORS: qué es, cómo funciona y errores de configuración explotables

Qué es CORS, cómo funciona Same-Origin Policy y preflight, headers Access-Control-*, siete errores de configuración explotables y configuración segura.

Secra10 de mayo de 202612 min de lectura

CORS (Cross-Origin Resource Sharing) es el mecanismo del navegador que permite a una página hacer peticiones a un origen distinto del suyo cuando el servidor destino lo autoriza explícitamente con cabeceras HTTP. Relaja la Same-Origin Policy (SOP) bajo control del servidor. Lo definió W3C en 2014 (hoy parte del Fetch Standard de WHATWG) y todo navegador moderno lo implementa. Cuando se configura mal, se convierte en una de las fuentes más frecuentes de vulnerabilidades web.

Esta guía explica qué problema resuelve CORS, cómo funciona internamente (request simple vs preflight), los headers que importan, los siete errores de configuración que aparecen una y otra vez en pentesting de aplicaciones web y APIs, casos reales documentados, configuración segura por escenario y cómo se prueba el correcto endurecimiento en una auditoría.

Qué problema resuelve CORS

Para entender CORS hay que entender primero la Same-Origin Policy (SOP): regla del navegador desde Netscape 2 (1995) que impide que código JavaScript cargado desde un origen lea datos de otro origen distinto. Origen se define por la tripleta protocolo + dominio + puerto. https://app.secra.es y https://api.secra.es son orígenes distintos; http://secra.es:80 y https://secra.es:443 también.

Sin SOP, una pestaña abierta en mal.example.com podría leer tu sesión activa en mibanco.com mediante un fetch JavaScript. SOP existe para impedirlo.

El problema es que muchas aplicaciones legítimas necesitan justo eso: el frontend SPA en app.secra.es consume la API en api.secra.es. SOP bloquearía la llamada por defecto. CORS es la forma estándar de que el servidor declare "acepto que app.secra.es me llame y lea mi respuesta".

Cómo funciona CORS

El protocolo distingue dos casos según la naturaleza de la petición.

Simple Request

Una petición es "simple" si cumple las tres condiciones:

  1. Método GET, HEAD o POST.
  2. Headers solo dentro de la lista CORS-safelisted (Accept, Accept-Language, Content-Language, Content-Type limitado a application/x-www-form-urlencoded, multipart/form-data o text/plain).
  3. Sin readable streams custom y sin event listeners en upload.

En este caso, el navegador envía la petición directamente, incluyendo el header Origin: https://app.secra.es. El servidor responde con la respuesta normal más el header Access-Control-Allow-Origin: https://app.secra.es (u otros valores que veremos abajo). Si el navegador no ve un header válido, descarta la respuesta y el JavaScript ve un error.

Importante: la petición se envía y se ejecuta en el servidor incluso si CORS la rechaza después. CORS no protege al servidor de side-effects, solo impide que el navegador entregue la respuesta al JavaScript origen.

Preflight

Cuando la petición no es simple (PUT, DELETE, headers custom, content-type JSON), el navegador envía primero una petición OPTIONS con cabeceras Access-Control-Request-Method y Access-Control-Request-Headers. El servidor responde con Access-Control-Allow-Methods y Access-Control-Allow-Headers declarando qué acepta. Si la respuesta autoriza, el navegador envía la petición real; si no, se aborta sin ejecutar.

El preflight es la diferencia entre "el navegador previene la fuga" y "el navegador previene incluso la ejecución del side-effect". Por eso Content-Type: application/json dispara preflight: cualquier llamada destructiva pasa por validación previa.

Headers que importan

Lista esencial para auditoría:

  • Access-Control-Allow-Origin. Origen autorizado. Puede ser un dominio concreto (https://app.secra.es), * (cualquiera, ojo) o null. Solo un valor; no se admite lista de dominios separados por coma.
  • Access-Control-Allow-Credentials. true permite enviar cookies y headers de autenticación con la petición. Crítico cuando hay sesión.
  • Access-Control-Allow-Methods. Métodos HTTP autorizados (GET, POST, PUT, DELETE, PATCH).
  • Access-Control-Allow-Headers. Headers custom que el cliente puede enviar (Authorization, X-Requested-With, etc.).
  • Access-Control-Expose-Headers. Headers de respuesta a los que el JavaScript del cliente puede acceder. Por defecto solo ve los CORS-safelisted; si la API devuelve un token en X-Custom-Token y quieres que el JS lo lea, hay que exponerlo.
  • Access-Control-Max-Age. Tiempo en segundos que el navegador puede cachear el preflight. Reduce overhead.

El navegador respeta estas cabeceras solo si las recibe del servidor; el cliente JavaScript no las puede falsificar para sí mismo.

Errores de configuración explotables

Los siete patrones que aparecen prácticamente en cualquier auditoría web profesional.

1. Allow-Origin wildcard con Allow-Credentials true

La combinación es inválida según especificación y los navegadores actuales la rechazan, pero existieron implementaciones tolerantes y todavía aparecen variantes peligrosas.

Lo más frecuente hoy: el servidor refleja el header Origin del cliente como valor de Access-Control-Allow-Origin y manda Allow-Credentials: true. Funcionalmente equivale a permitir cualquiera con cookies. Cualquier sitio que el usuario visite puede leer la respuesta autenticada de la API.

Detección: enviar petición con Origin: https://atacante.com. Si la respuesta devuelve Access-Control-Allow-Origin: https://atacante.com, vulnerable.

2. Reflexión del Origin sin lista blanca

Implementación naive del backend: headers["Access-Control-Allow-Origin"] = request.headers["Origin"]. Pasa cualquier dominio incluyendo el del atacante.

Variante con whitelist mal hecha: if "secra.es" in origin: permite https://atacante-secra.es.evil.com o https://secra.es.evil.com. El startsWith/endsWith mal pensado abre la puerta.

3. Confianza en subdominios sin control de DNS

https://*.secra.es parece razonable hasta que un dominio antiguo (olvido.secra.es) tiene CNAME apuntando a un servicio de terceros caducado. El atacante reclama el subdominio, levanta un sitio en él y sus peticiones cumplen la regla CORS.

Aplica también a "cualquier subdominio de empresa-amiga.com": basta una vulnerabilidad XSS en un subdominio cualquiera de la lista para escalar a la API protegida.

4. null permitido

Algunos backends incluyen null en la whitelist por error. Origen null se envía cuando la petición viene de:

  • Un fichero local cargado en el navegador.
  • Iframe sandboxed sin atributo allow-same-origin.
  • Documento data URI.

Un atacante puede forzar origen null con un iframe sandbox controlado y leer la respuesta autenticada.

5. CORS solo en una ruta

/api/v1/usuarios tiene CORS estricto pero /api/v1/usuarios/avatar no. El atacante encuentra el endpoint olvidado y lo explota. Aparece típicamente cuando se añaden endpoints sin pasar por el middleware central CORS.

6. Cabeceras CORS en respuesta a errores

El backend responde con headers CORS solo a 200 OK. La página de error 404 no tiene headers CORS pero el cuerpo devuelve información sensible. Algunos frameworks tienen este bug por defecto.

7. Preflight cacheado demasiado tiempo con Allow-Headers wildcard

Access-Control-Allow-Headers: * con Max-Age: 86400 permite que el navegador cachee 24 horas la autorización de cualquier header. Si el desarrollador endurece después la política, los clientes con cache válido siguen pudiendo enviar headers que ya no deberían.

Casos de explotación reales

CORS mal configurado abre escenarios concretos según el contexto.

Robo de datos privados desde una API autenticada. La API confía en cookies de sesión, el origen vulnerable refleja Origin. El atacante aloja JavaScript en su sitio que hace fetch(api_endpoint, { credentials: "include" }). El navegador del usuario logueado envía las cookies, la API responde, el atacante recibe los datos. Sin XSS de por medio.

Escalada de XSS limitado a takeover de cuenta. Un XSS en un subdominio menor se traduce en compromiso de toda la API si la regla CORS confía en *.secra.es.

Bypass de defensas anti-CSRF débiles. Aplicaciones que confían en CORS como única protección anti-CSRF en lugar de tokens dedicados. Con CORS roto, la defensa cae.

Robo de tokens JWT almacenados en localStorage. Si la API expone endpoint que devuelve datos sensibles incluyendo tokens, y CORS está mal, el atacante puede leer el JWT del usuario sin ataque al endpoint en sí.

Information disclosure en respuestas de error. Endpoints que devuelven mensajes de error con datos sensibles (nombres de tablas, paths, versiones) y CORS abierto exponen reconnaissance.

Un caso documentado público: el bug bounty de Bitwarden en 2018 reveló que vault.bitwarden.com aceptaba null como origen, lo que permitía a un sitio externo leer respuestas autenticadas mediante iframes sandbox. Reportado por Tavis Ormandy, fixeado en horas.

Configuración segura por escenario

Los patrones que aplica un equipo de auditoría web experimentado.

API pública sin autenticación

Sin sesión ni cookies. Aceptable Allow-Origin: * sin Allow-Credentials. Adecuado para APIs de datos públicos (precios, stock público, datos abiertos administrativos).

API con autenticación por header (Bearer token)

El JS cliente envía Authorization: Bearer <jwt>. CORS necesita autorizar el header (Allow-Headers: Authorization) y el origen del frontend (Allow-Origin: https://app.secra.es). No se necesita Allow-Credentials porque no hay cookies. Es el patrón típico de SPA moderna.

Aquí no se puede usar wildcard. La política tiene que ser:

  • Access-Control-Allow-Origin: https://app.secra.es (origen específico)
  • Access-Control-Allow-Credentials: true
  • Access-Control-Allow-Methods: GET, POST, PUT, DELETE
  • Access-Control-Allow-Headers: Content-Type, X-CSRF-Token

Y combinar con tokens CSRF dedicados, no confiar solo en CORS.

Multi-tenant con muchos orígenes

Whitelist explícita en backend (no regex), validación estricta del Origin, devolver el dominio concreto de la lista, no reflejar.

const allowedOrigins = [
  "https://app.secra.es",
  "https://admin.secra.es",
  "https://socio-confianza.com"
];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
  res.setHeader("Access-Control-Allow-Origin", origin);
  res.setHeader("Vary", "Origin");
}

El header Vary: Origin es crítico: sin él, los caches CDN pueden servir la respuesta autorizada para app.secra.es también a otro origen, anulando la protección.

Subdominios con DNS estricto

Si quieres aceptar *.secra.es, mantén inventario actualizado, retira subdominios que no se usen, vigila certificados TLS de la organización con crt.sh o monitorización Censys, y evita CNAMEs hacia servicios externos sin propietario claro.

Cómo se prueba en una auditoría

Un pentester de aplicaciones web ejecuta sistemáticamente:

  1. Mapa de orígenes. Enumera todos los endpoints de API que el frontend consume.
  2. Inyección de Origin. Por cada endpoint, repite la petición con varios valores de Origin: dominio del frontend, atacante.com, null, https://app.secra.es.evil.com, https://evil.app.secra.es.
  3. Análisis de respuesta. Anota qué valor de Access-Control-Allow-Origin devuelve cada caso, si manda Allow-Credentials: true, si hay Vary: Origin.
  4. Test de preflight. Por cada endpoint con verbo no-simple, comprueba el OPTIONS y los headers permitidos.
  5. Test de regex. Prueba variantes que rompan whitelist mal escritas: prefijos, sufijos, caracteres especiales.
  6. PoC explotable. Para cualquier hallazgo confirmado, construye un PoC HTML que demuestre la lectura de respuesta autenticada desde origen externo. Lo entrega como evidencia reproducible.

Herramientas habituales: Burp Suite con extensiones (CORS, Param Miner), CORScanner, ffuf con headers custom, scripts custom Python.

Detalles operativos en la guía de pentesting de aplicaciones web y en el cluster OWASP Top 10 2025 (fallos de configuración A05:2021).

Encaje con compliance

CORS mal configurado materializa riesgos que cubren los marcos:

  • NIS2 (artículo 21). Medidas técnicas de gestión de riesgos. Una API expuesta vía CORS abierto incumple el deber de protección razonable.
  • DORA (artículo 9). Resiliencia operacional digital de servicios financieros. Las APIs son ICT crítico.
  • ISO 27001:2022 (control 8.26). Application security requirements.
  • PCI DSS v4.0 (req. 6.4). Web application protection. CORS se incluye en la auditoría de aplicaciones de pago.
  • RGPD. Una vulnerabilidad CORS que filtre datos personales constituye brecha notificable.

Preguntas frecuentes

¿CORS protege al servidor o al usuario?

Al usuario, mediante el navegador. El servidor sigue ejecutando la petición; CORS solo impide que el JavaScript del origen no autorizado lea la respuesta. Por eso CORS no es defensa contra side-effects ni sustituye a CSRF tokens, autenticación o autorización.

¿CORS aplica a peticiones desde curl o Postman?

No. CORS lo enforce el navegador. Una petición desde curl, Postman, un script de servidor o cualquier cliente no-navegador llega al backend y se ejecuta sin restricciones CORS. Por eso la API debe tener auth, autorización, rate limiting y validación de input independientes.

¿Es seguro usar Allow-Origin: *?

Solo si la API no requiere autenticación y los datos son verdaderamente públicos. Para cualquier endpoint con sesión, cookies o token de autorización, el wildcard es vulnerabilidad.

¿Diferencia entre CORS y CSP?

CORS controla qué orígenes pueden leer recursos cross-origin (relaja SOP). Content-Security-Policy controla qué orígenes pueden cargar recursos en una página (restringe lo que el navegador ejecuta). Son complementarios, no se sustituyen.

¿Qué pasa con WebSockets, fetch streaming, EventSource?

WebSockets no usan CORS sino su propio handshake con header Origin que el servidor valida manualmente. EventSource sí respeta CORS. Fetch streaming sigue las reglas CORS estándar más restricciones específicas para streams.

¿Cómo configuro CORS en Next.js, Express o FastAPI?

  • Next.js App Router: middleware o headers() en next.config.js para rutas API; o Response con headers explícitos en route handlers.
  • Express: paquete cors con whitelist explícita.
  • FastAPI: middleware CORSMiddleware con allow_origins lista.
  • Spring Boot: anotación @CrossOrigin con origins específicos o configuración global vía WebMvcConfigurer.

En todos los casos, evitar reflejos automáticos del header Origin y validar contra lista cerrada.

¿Necesita auditarse CORS en cada release?

Como mínimo en cada cambio de arquitectura: añadir un nuevo subdominio, integrar un proveedor third-party, refactorizar middleware de seguridad, separar microservicios. Y en cada auditoría externa periódica como parte estándar.

Recursos relacionados

Auditoría CORS en Secra

En Secra revisamos CORS como parte estándar de cualquier auditoría web o API: enumeración de endpoints, test sistemático de whitelist con orígenes adversariales, validación de preflight, comprobación de headers en respuestas de error, verificación de Vary: Origin y caching CDN, y entregable con PoC reproducible para cada hallazgo. Si tu equipo va a desplegar una API nueva, está migrando a SPA moderna o no ha auditado nunca la política CORS de su frontend, escríbenos a través de contacto o consulta nuestro servicio de auditoría web y móvil.

Sobre el autor

Equipo de Secra Solutions

Ethical hackers certificados OSCP, OSEP, OSWE, CRTO, CRTL y CARTE, con más de 7 años de experiencia en ciberseguridad ofensiva. Autores de los CVE-2025-40652 y CVE-2023-3512.

Compartir artículo