Aikido

Seguridad CORS: Más allá de la configuración básica

Rez MossRez Moss
|
No se encontraron elementos.

Todos hemos pasado por eso: envías una solicitud API, esperas la respuesta y, de repente, aparece el« errorCORS» en la consola de tu navegador.

Para muchos desarrolladores, el primer instinto es buscar una solución rápida: añadir Access-Control-Allow-Origin: * y seguir adelante. Sin embargo, ese enfoque pasa por alto lo esencial. CORS no es solo otro obstáculo de configuración, sino uno de los mecanismos de seguridad de navegador más importantes jamás creados.

CORS, o Cross-Origin Resource Sharing (Compartir recursos entre orígenes), existe para proteger a los usuarios al tiempo que permite la comunicación legítima entre dominios entre aplicaciones web. Sin embargo, a menudo se malinterpreta, se configura incorrectamente o se trata como un error que hay que «eludir».

Pero ya no.

En esta guía, iremos más allá de lo básico. Aprenderás:

  • ¿Por qué existe CORS y cómo evolucionó a partir de la política del mismo origen (SOP)?
  • Cómo negocian realmente los navegadores y los servidores el acceso entre orígenes
  • ¿Qué hace que algunas configuraciones CORS fallen, incluso cuando «parecen correctas»?
  • Cómo gestionar de forma segura las solicitudes previas al vuelo, las credenciales y las peculiaridades del navegador.

Al final, no solo sabrás cómo configurar CORS, sino que también comprenderás por qué se comporta de esa manera y cómo diseñar tus API de forma segura en torno a él.

¿Qué es CORS (y por qué existe)? 

CORS es un estándar de seguridad para navegadores que define cómo las aplicaciones web de un origen pueden acceder de forma segura a los recursos de otro.

Para comprender la seguridad CORS, primero hay que saber por qué se creó.

Mucho antes de que las API y los microservicios se impusieran en la web, los navegadores seguían una regla sencilla denominada «política del mismo origen» (SOP, por sus siglas en inglés).

Esta política establecía que una página web solo podía enviar y recibir datos del mismo origen, es decir, del mismo protocolo, dominio y puerto.

Por ejemplo:

Tabla comparativa del mismo origen

URL A URL B ¿El mismo origen?
https://example.com/api https://example.com/users ✅ Sí
https://example.com http://example.com ❌ No (protocolo diferente)
https://example.com https://api.example.com ❌ No (host diferente)
https://example.com https://example.com:8080 ❌ No (puerto diferente)

Esta restricción tenía mucho sentido en los inicios de la web, cuando la mayoría de los sitios web eran monolíticos. Un único sitio alojaba su interfaz, su back-end y sus activos, todo bajo un mismo dominio.

Pero a medida que la web evolucionó con API, microservicios e integraciones de terceros, esta misma regla se convirtió en una barrera. Los desarrolladores necesitaban aplicaciones front-end para comunicarse con otros dominios, tales como:

  • www.example.com hablando con api.example.com
  • Tu aplicación conectada a un CDN o punto final de análisis
  • Clientes web que llaman a API de terceros (como Stripe o Google Maps)

La política del mismo origen se convirtió en un muro que bloqueaba las arquitecturas modernas y distribuidas.

Ahí es donde entró en juego el Cross-Origin Resource Sharing (CORS).

En lugar de eliminar por completo las restricciones del navegador, CORS introdujo una flexibilización controlada del SOP. Creó una forma segura para que los navegadores y los servidores se comunicaran entre dominios, de forma segura y solo cuando ambas partes estuvieran de acuerdo.

Piénsalo así: SOP es una puerta cerrada que no deja entrar a nadie, y CORS es la misma puerta, pero con una lista de invitados y un portero que comprueba los documentos de identidad.

Este equilibrio entre flexibilidad y protección es lo que hace que la configuración CORS sea fundamental para todas las aplicaciones web modernas.

Comprender la política del mismo origen (SOP)

Antes de profundizar en la configuración de CORS, es fundamental comprender su base: la política del mismo origen (SOP).

Como se ha mencionado anteriormente, el SOP es la primera línea de defensa del navegador contra comportamientos maliciosos en la web. Impide que un sitio web acceda libremente a los datos de otro, lo que podría exponer información confidencial como cookies, tokens de autenticación o datos personales.

Así es como funciona en la práctica: cuando una página web se carga en tu navegador, se le asigna un origen basado en tres elementos: el protocolo, el host y el puerto:

https://   api.example.com   :443
^          ^                 ^
protocolo   host              puerto

Dos URL se consideran del mismo origen solo si coinciden estas tres partes. De lo contrario, el navegador las trata como de origen cruzado.

Esta sencilla regla impide acciones perjudiciales entre sitios web. Sin ella, cualquier sitio web podría cargar el panel de control de tu banca online en un marco invisible, leer tu saldo y enviarlo a un atacante, todo ello sin tu consentimiento.

En resumen, SOP existe para aislar el contenido entre diferentes sitios, garantizando que cada origen sea una zona de seguridad autónoma.

Por qué el SOP por sí solo no era suficiente

La política del mismo origen funcionaba perfectamente cuando los sitios web eran autónomos. Sin embargo, a medida que la web evolucionó hacia un ecosistema de API, microservicios y arquitecturas distribuidas, esta estricta norma se convirtió en una limitación importante.

Las aplicaciones modernas deben:

  • Llamar a sus propias API alojadas en diferentes subdominios (app.example.com → api.example.com)
  • Obtener activos de CDN o servicios de terceros
  • Integración con API externas como Stripe, Firebase o Google Maps.

Bajo el SOP, estas solicitudes legítimas de origen cruzado fueron bloqueadas. Los desarrolladores intentaron todas las soluciones posibles, incluyendo JSONP, proxies inversos o dominios duplicados, pero estas soluciones eran inseguras o muy complejas.

Ahí es donde CORS (Cross-Origin Resource Sharing) cambió las reglas del juego.

CORS introdujo un sistema de protocolo de enlace que permitía a los navegadores y servidores negociar la confianza. En lugar de romper el SOP, lo amplió, proporcionando una forma de incluir de forma segura en la lista blanca orígenes específicos para la comunicación entre dominios.

Cómo funciona CORS: el flujo a nivel de protocolo

Como se ha mencionado anteriormente, cuando tu navegador realiza una solicitud a un origen diferente, no la envía sin más. En su lugar, sigue un protocolo CORS bien definido: una conversación bidireccional entre el navegador y el servidor para determinar si se debe permitir la solicitud.

En esencia, CORS funciona a través de encabezados HTTP. El navegador adjunta un encabezado Origin a cada solicitud de origen cruzado, indicando al servidor de dónde proviene la solicitud. A continuación, el servidor responde con uno o varios encabezados Access-Control-* que definen lo que está permitido.

Aquí hay un ejemplo simplificado de esa conversación:

# Request
GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com

# Response
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json

{"message": "Success"}
Flujo CORS de solicitud y respuesta
Flujo CORS de solicitud y respuesta

En este caso, el servidor permite explícitamente que el origen https://app.example.com acceda a su recurso. El navegador comprueba esta respuesta, confirma la coincidencia y envía los datos a tu JavaScript.

Pero si los orígenes no coinciden o si los encabezados de respuesta faltan o son incorrectos, el navegador bloquea silenciosamente la respuesta. No verás los datos, solo ese frustrante mensaje de «error CORS» en tu consola.

Es importante señalar que CORS no hace que un servidor sea más seguro por sí mismo. En cambio, aplica reglas de interacción entre navegadores y servidores, una capa de seguridad que garantiza que solo los orígenes de confianza puedan acceder a los recursos protegidos.

Tipos de solicitudes CORS

CORS define dos tipos principales de solicitudes: simples y preflighted. La diferencia radica en la cantidad de verificación que realiza el navegador antes de enviar los datos.

1. Solicitudes sencillas

Una solicitud simple es el tipo más sencillo. Los navegadores la permiten automáticamente siempre que siga unas reglas específicas:

  • Utiliza uno de estos métodos: GET, HEAD o POST.

  • Incluye solo determinados encabezados:

    • Aceptar
    • Aceptar idioma
    • Idioma del contenido
    • Tipo de contenido (pero solo application/x-www-form-urlencoded, multipart/form-data o text/plain)
  • No utiliza encabezados ni flujos personalizados.

Así es como se ve:

# Request
GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com

# Response
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json

{"message": "This is the response data"}

En este caso:

  • El navegador añade automáticamente el encabezado Origin.
  • El servidor debe devolver Access-Control-Allow-Origin con un origen coincidente.
  • Si el origen no coincide o falta, el navegador bloquea la respuesta.

2. Solicitudes previas al vuelo

Las cosas se ponen más interesantes con solicitudes no simples. Por ejemplo, cuando se utilizan métodos como PUT, DELETE o encabezados personalizados como Authorization.

Antes de enviar la solicitud real, el navegador realiza una comprobación previa utilizando una solicitud OPTIONS. Este paso garantiza que el servidor permita explícitamente la operación prevista.

He aquí un ejemplo:

# Preflight Request
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type, authorization

# Preflight Response
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: PUT, POST, GET, DELETE
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Max-Age: 3600

# Actual Request (only sent if preflight succeeds)
PUT /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Authorization: Bearer token123

{"data": "update this resource"}

En esta secuencia:

  1. El navegador detecta una solicitud no simple.
  2. Envía una solicitud OPTIONS previa, solicitando permiso para el método y los encabezados reales.
  3. El servidor responde con los métodos, encabezados y orígenes que permite.
  4. Si la comprobación previa al vuelo se supera, el navegador envía la solicitud real. De lo contrario, la bloquea.

Manejo de credenciales en CORS

Cuando se trata de API que requieren autenticación, como cookies, tokens o inicios de sesión basados en sesiones, CORS se comporta de manera diferente.

Por defecto, los navegadores tratan las solicitudes de origen cruzado como no autenticadas por motivos de seguridad. Esto significa que las cookies o los encabezados de autenticación HTTP no se incluyen automáticamente.

Para habilitar las solicitudes con credenciales de forma segura, deben alinearse dos pasos clave:

1. El cliente debe permitir explícitamente las credenciales:

fetch('https://api.example.com/data', {
  credentials: 'include'
})

2. El servidor debe permitirlo explícitamente:

Control de acceso: Permitir credenciales: true

Pero hay un inconveniente, y es uno grande.

Cuando Access-Control-Allow-Credentials se establece en true, no se puede utilizar un comodín (*) en Access-Control-Allow-Origin. Los navegadores rechazarán la respuesta si lo intentas.

Esto se debe a que permitir que todos los orígenes envíen solicitudes con credenciales frustraría todo el propósito de la seguridad CORS, ya que permitiría que cualquier sitio de Internet accediera a datos privados vinculados a la sesión de un usuario.

Así que en lugar de esto:

Control de acceso: Permitir origen: *
Control de acceso: Permitir credenciales: true

Siempre debes utilizar un origen específico:

Control de acceso: permitir origen: https://tuapp.com
Control de acceso: Permitir credenciales: true

Si su API presta servicio a varios dominios de confianza, puede devolver dinámicamente el encabezado de origen correcto en el lado del servidor:

const allowedOrigins = ['https://app1.com', 'https://app2.com'];
const origin = req.headers.origin;

if (allowedOrigins.includes(origin)) {
  res.setHeader('Access-Control-Allow-Origin', origin);
  res.setHeader('Access-Control-Allow-Credentials', 'true');
}

Este enfoque garantiza que tus solicitudes autenticadas permanezcan seguras e intencionadas, sin estar abiertas a cualquiera que lo intente.

Cómo determinan los navegadores qué solicitudes son aptas para CORS

Antes de que una solicitud llegue a tu servidor, el navegador decide si se ajusta a las reglas CORS.

Esta decisión depende del origen de la solicitud y de si esta se dirige a otro dominio, puerto o protocolo.

Por ejemplo:

  • Solicitud de https://api.example.com desde una página servida en https://example.com: ✅ Se aplica CORS (subdominio diferente).
  • Solicitando https://example.com:3000 desde https://example.com: ✅ Se aplica CORS (puerto diferente).
  • Solicitud de https://example.com desde el mismo dominio y puerto: ❌ CORS no se aplica.

Si el navegador detecta que una solicitud cruza orígenes, incluye automáticamente el encabezado Origin en la solicitud:

Origen: https://example.com

Este encabezado le indica al servidor de dónde proviene la solicitud y es lo que el servidor utiliza para decidir si permite o bloquea el acceso.

Si la respuesta carece de los encabezados adecuados (como Access-Control-Allow-Origin), el navegador simplemente bloquea el acceso a la respuesta, aunque técnicamente el servidor haya enviado una.

Esa es una distinción importante: el navegador aplica CORS, no el servidor.

Comprobaciones de seguridad interna, XMLHttpRequest frente a Fetch y diferencias entre navegadores

No todos los navegadores gestionan CORS de la misma manera, pero todos siguen el mismo modelo de seguridad: nunca confiar en datos de origen cruzado a menos que se permita explícitamente.

Lo que difiere es el grado de rigor con el que se aplican las normas y a qué API se aplican.

1. La comprobación interna de seguridad CORS

Cuando un navegador recibe una respuesta a una solicitud de origen cruzado, realiza un paso de validación interna antes de exponer la respuesta a su código JavaScript.

Comprueba si hay encabezados como:

  • Access-Control-Allow-Origin: debe coincidir con el origen de la solicitud (o ser * en algunos casos).
  • Access-Control-Allow-Credentials: debe ser verdadero si se utilizan cookies o tokens de autenticación.
  • Access-Control-Allow-Methods y Access-Control-Allow-Headers: deben coincidir con la solicitud previa original, si se envió alguna.

Si alguna de estas comprobaciones falla, el navegador no genera un error HTTP, sino que simplemente bloquea el acceso a la respuesta y registra un error CORS en la consola.

Esto dificulta la depuración, ya que la solicitud de red real se ha realizado correctamente, pero el navegador oculta el resultado por motivos de seguridad.

2. XMLHttpRequest frente a Fetch

Tanto XMLHttpRequest como la moderna API fetch() son compatibles con CORS, pero se comportan de forma ligeramente diferente en lo que respecta a las credenciales y los valores predeterminados.

Con XMLHttpRequest:

  • Las cookies y la autenticación HTTP se envían automáticamente si withCredentials se establece en true.
  • El comportamiento previo al vuelo depende de si se añaden encabezados personalizados.

Con Fetch:

  • Las credenciales (cookies, autenticación HTTP) no se incluyen de forma predeterminada.
  • Debe habilitarlos explícitamente utilizando:
fetch("https://api.example.com/data", {
	credentials: "include"
});
  • fetch también trata las redirecciones de forma más estricta en CORS, ya que no seguirá las redirecciones entre orígenes a menos que se permita.

Por lo tanto, aunque fetch es más limpio y moderno, también es menos tolerante cuando se olvida un encabezado o se omite una regla de credenciales.

3. Diferencias y peculiaridades de los navegadores

Aunque la especificación CORS es estándar, los navegadores la implementan con sutiles diferencias:

  • Safari puede ser demasiado estricto con las cookies y las solicitudes de credenciales, especialmente cuando se bloquean las cookies de terceros.
  • Firefox a veces almacena en caché las respuestas fallidas de preflight durante más tiempo del esperado, lo que da lugar a resultados inconsistentes durante las pruebas.
  • Chrome aplica CORS en ciertas cadenas de redireccionamiento de forma más agresiva que en otras.

Debido a estas diferencias, una configuración que funciona perfectamente en un navegador puede fallar silenciosamente en otro.

Por eso es fundamental probar las configuraciones CORS en todos los navegadores, especialmente cuando hay credenciales o redireccionamientos involucrados.

Gestión del encabezado Origin por parte del servidor

Aunque el navegador aplica CORS, la toma de decisiones real se produce en el servidor.

Cuando el navegador envía una solicitud de origen cruzado, siempre incluye el encabezado Origin. La función del servidor es inspeccionar ese encabezado, decidir si lo permite y devolver los encabezados CORS correctos en respuesta.

1. Validación del origen

Una solicitud típica podría ser así: Origen: https://frontend.example.com

En el servidor, tu código debe comprobar si este origen está permitido. El método más sencillo (y seguro) es mantener una lista de dominios de confianza:

const allowedOrigins = ["https://frontend.example.com", "https://admin.example.com"];
if (allowedOrigins.includes(req.headers.origin)) {
  res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
}

Esto garantiza que solo los clientes conocidos puedan acceder a tu API, mientras que los demás no reciben ningún permiso CORS.

Evita devolver Access-Control-Allow-Origin: * si tu API maneja cookies, tokens u otras credenciales.

2. Gestión de solicitudes previas al vuelo

Para las solicitudes de comprobación previa OPTIONS, el servidor debe responder con el mismo cuidado que para las solicitudes principales.
Una respuesta de comprobación previa completa incluye:

Control de acceso: permitir origen: https://frontend.example.com
Control de acceso: permitir métodos: GET, POST, OPTIONS
Control de acceso: permitir encabezados: tipo de contenido, autorización
Control de acceso: tiempo máximo: 86400

Estos encabezados le indican al navegador qué está permitido y durante cuánto tiempo puede almacenar esa decisión en la caché. Si alguno de ellos falta o es incorrecto, el navegador bloqueará la solicitud de seguimiento, incluso si el punto final en sí funciona correctamente.

3. Configuración dinámica de encabezados CORS

En sistemas grandes (como plataformas multitenant o API con múltiples clientes), es posible que los orígenes permitidos deban ser dinámicos.

Por ejemplo:

const origin = req.headers.origin;

if (origin && origin.endsWith(".trustedclient.com")) {
  res.setHeader("Access-Control-Allow-Origin", origin);
}

Este patrón permite todos los subdominios de un dominio de confianza, al tiempo que sigue filtrando las fuentes desconocidas.

Solo asegúrate de validar cuidadosamente los orígenes y no comparar cadenas en las entradas de los usuarios sin restricciones, o los atacantes podrían falsificar encabezados que parezcan válidos.

4. Por qué «funciona en Postman» no significa que esté configurado correctamente

Uno de los mayores conceptos erróneos sobre CORS es el siguiente: «Funciona en Postman, por lo que debe tratarse de un problema del navegador».

Postman no aplica CORS en absoluto, ya que no es un navegador.

Esto significa que incluso una API completamente abierta sin encabezados Access-Control-* responderá correctamente allí, pero fallará inmediatamente en Chrome o Firefox.

Por lo tanto, si tu API funciona en Postman pero no en tu aplicación web, es probable que tus encabezados CORS estén incompletos o mal configurados.

Configuraciones incorrectas comunes de CORS (y cómo evitarlas)

1. Uso de Access-Control-Allow-Origin: * con credenciales

Este es el error más frecuente y peligroso.
Si tu respuesta incluye ambos:

Control de acceso: Permitir origen: *
Control de acceso: Permitir credenciales: true

... el navegador bloqueará la solicitud automáticamente.

La especificación CORS prohíbe el uso de comodines cuando se incluyen credenciales, ya que permitiría a cualquier sitio acceder a los datos de usuario vinculados a cookies o tokens de autenticación.

Solución: Devolver siempre un origen específico cuando se utilicen credenciales:

Control de acceso: permitir origen: https://app.example.com
Control de acceso: Permitir credenciales: true

2. Olvidarse de gestionar las solicitudes previas al vuelo

Muchas API responden correctamente a GET y POST, pero se olvidan de la solicitud previa OPTIONS.

Cuando eso ocurre, el navegador nunca llega a tu punto final real y bloquea la solicitud principal tras el fallo de la comprobación previa.

Solución: gestionar explícitamente las solicitudes OPTIONS y responder con los encabezados correctos:

if (req.method === "OPTIONS") {
  res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
  res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization");
  return res.sendStatus(204);
}

3. Encabezados de solicitud y respuesta desalineados

Otra cuestión sutil: tu solicitud previa al vuelo puede solicitar ciertos encabezados, pero el servidor no los permite explícitamente.

Por ejemplo, si su solicitud incluye:

Encabezados de solicitud de control de acceso: Autorización, Tipo de contenido

…pero el servidor solo responde con:

Control de acceso: permitir encabezados: tipo de contenido

... el navegador lo bloquea. Ambas listas deben coincidir exactamente.

Solución: Asegúrate de que tu Access-Control-Allow-Headers incluya todos los encabezados que el cliente pueda enviar, especialmente Authorization, Accept y los encabezados personalizados.

4. Devolución de múltiples encabezados Access-Control-Allow-Origin

Algunos proxies o marcos mal configurados envían múltiples encabezados Access-Control-Allow-Origin (por ejemplo, uno estático * y otro dinámico).

Los navegadores lo consideran inválido y bloquean la solicitud por completo.

Solución: Devuelve siempre un único encabezado Access-Control-Allow-Origin válido.

5. Olvidarse de las restricciones del método

Si no se incluyen todos los métodos permitidos en Access-Control-Allow-Methods, los navegadores rechazarán las solicitudes legítimas.

Por ejemplo, una API puede admitir PUT, pero su respuesta previa solo permite GET y POST.

Solución: Enumere todos los métodos compatibles o haga coincidir dinámicamente sus rutas API para garantizar la coherencia.

6. Ignorar las respuestas de comprobación previa almacenadas en caché

Los navegadores modernos almacenan en caché los resultados de la comprobación previa para mejorar el rendimiento.
Pero si tu servidor o CDN almacena en caché las respuestas sin variar según el origen, podrías enviar accidentalmente encabezados CORS incorrectos a otro cliente.

Solución: Utilice el encabezado Vary: Origin para garantizar que las respuestas se almacenen en caché por separado según el origen.

Los problemas de CORS rara vez se deben a un solo error grave. Por lo general, son el resultado de varios pequeños desajustes entre las expectativas del navegador y la configuración del servidor. Comprender estos patrones te ayudará a evitar interminables ciclos de depuración de «errores CORS».

CORS no es el enemigo, sino el malentendido.

A primera vista, CORS parece una barrera innecesaria, o más bien un guardián que interrumpe tus solicitudes y ralentiza el desarrollo.

Pero, en realidad, es una de las funciones de seguridad más importantes que se han creado nunca para los navegadores.

Una vez que comprendes cómo funciona, dejas de ver los «errores CORS» como fallos aleatorios y, en su lugar, se convierten en señales de que tu cliente y tu servidor necesitan alinearse mejor en cuanto a confianza, encabezados o credenciales.

Tanto si estás creando una aplicación de una sola página como un ecosistema API distribuido, CORS es tu aliado para mantener la seguridad de los usuarios y permitir una comunicación segura entre dominios.

Así que la próxima vez que te encuentres con ese mensaje familiar de la consola, no recurras al comodín. Lee los encabezados, sigue la lógica y deja que tu comprensión, y no un truco aleatorio, guíe la solución.

4.7/5

Protege tu software ahora.

Empieza gratis
Sin tarjeta
Solicitar una demo
Sus datos no se compartirán · Acceso de solo lectura · No se requiere tarjeta de crédito

Asegúrate ahora.

Proteja su código, la nube y el entorno de ejecución en un único sistema central.
Encuentre y corrija vulnerabilidades de forma rápida y automática.

No se requiere tarjeta de crédito | Resultados del escaneo en 32 segundos.
No se encontraron elementos.