CORS. Qué es, cómo funciona, para qué sirve y cómo solucionarlo

Si diseñas aplicaciones web que consumen API, estoy seguro de que alguna vez te has encontrado el problema de Cross origin resource sharing (CORS). En este documento veremos por qué ocurre, en qué se basa y cómo se puede solucionar.
CORS. Qué es, cómo funciona, para qué sirve y cómo solucionarlo

Cuando hablamos de CORS (Cross-Origin Resource Sharing en inglés, que en español sería algo así como intercambio de recursos de origen cruzado) nos referimos a un mecanismo de seguridad que aplican los navegadores cuando estamos haciendo una petición a un recurso que está alojado en otro origen. Si el recurso está en otro origen, el navegador automáticamente comprobará las cabeceras HTTP buscando una autorización expresa por parte del servidor.

Por repasar, te recuerdo que el concepto origen es la suma de protocolo, dominio y puerto. Es decir, CORS aplica cuando el origen es diferente, por lo tanto se comprueba que sea el mismo protocolo (http o https), el mismo dominio (sin incluir subdominios) y el mismo puerto (80, 443, 8080, etc.). Si el origen que hace la petición no coincide con el origen del recurso entra CORS en juego.

Por ejemplo, imagina que estás diseñando una aplicación que está ubicada en miapp.es y las peticiones de datos las realiza a una API, situada en api.miapp.es. En cuanto vayas a hacer una consulta con el método fetch de JS al dominio (o subdominio donde en este caso tienes la API) te aparecerá el siguiente error:

IMAGEN DE CORS Ejemplo de error de CORS

Esto no solo ocurre cuando la llamada la hace Javascript. También verás errores de CORS cuando intentes cargar Web Fonts (usando @font-face), vídeos, texturas, etc.

A continuación veremos qué es exactamente CORS, de qué te puede proteger y cómo quitar este error. Y una cosa importante: los errores de CORS están al nivel del navegador, por lo que no podrás hacer debug utilizando JS, así que el único mecanismo de depuración es mediante las Dev Tools 🤷‍♂️.

¿De qué protege Cross origin resource sharing?

Pues en muy resumidas cuentas, CORS protege de que sitios maliciosos interactúen con sitios legítimos. Te pongo un ejemplo si no existiese CORS: imagina que estás navegando por Internet y entras en un sitio legítimo como es Facebook. Por cualquier cosa, mientras navegas por Facebook (donde ya tienes una sesión iniciada), haces clic en un post que te lleva a una página maliciosa como SitioMalo.com. Pasará lo siguiente:

  • SitioMalo.com descarga y ejecuta en tu navegador un código javascript malicioso que emite una petición a Facebook (como por ejemplo alguna petición que fuerce a Facebook a devolver tus token, tus credenciales, tu email, etc.)

  • Facebook recibe la petición de SitioMalo.com y al no existir CORS, nadie se encarga de validar si ese origen es legítimo y puede realizar peticiones. Así que Facebook devuelve tus datos al script de SitioMalo.com (porque la única forma de comprobar que eres tú es que haya iniciado sesión, y tú lo has hecho a través del navegador).

  • SitioMalo.com recibe tus datos y aquí comienza el problema: listas de SPAM, estafa bancaria y demás problemas de seguridad de hoy en día.

Gracias a CORS, en el punto dos de la lista, Facebook devolvería una cabecera CORS que solo autoriza a enviar peticiones a ciertos sitios web autorizados y nunca a SitioMalo.com. Por ello, tu navegador bloquea automáticamente la respuesta y SitioMalo.com jamás podrá leer los datos que Facebook ha devuelto.

Por ello, tienes que ver la política de CORS y same-site como tu aliado en seguridad; nunca como algo que estorba. La seguridad de los usuarios dependen en cierta parte de que CORS (y otras políticas) funcionen y de que los desarrolladores sigamos escrupulosamente.

¡Vale! Llegados a este punto, creo que estás listo para entender qué es eso de CORS.

¿Qué es CORS? Solicitudes Simples y Solicitudes Verificadas

Existen dos tipos de CORS, o mejor dicho, dos formas de actuar basándonos en el tipo de petición que se realice. Empecemos por las Solicitudes Simples.

Solicitudes Simples. El CORS más amigable

En este tipo de solicitudes, el navegador hace la petición y espera la respuesta. Cuando esta ha llegado, analiza las cabeceras que el servidor web nos ha enviado y busca la cabecera CORS de Access-Control-Allow-Origin. No buscará ninguna cabecera CORS más.

Básicamente, esta cabecera indica qué orígenes (qué direcciones web) pueden leer la respuesta que acaba de mandar. Si nuestra web está en "miapp.es" y existe la cabecera "Access-Control-Allow-Origin: miapp.es", ¡genial! Puedes mostrar el contenido. En caso contrario, el navegador desecha la respuesta y devuelve un error a Javascript.

Para que se aplique el protocolo CORS de solicitud simple, se deben dar los siguientes requisitos de forma obligatoria:

  • Tiene que ser una llamada hecha con los métodos HEAD, GET o POST.

  • Aparte de las cabeceras que automáticamente añaden los navegadores (y sobre las que no tenemos control como es el caso de Origin, Connection o User-Agent), la petición solo puede llevar como mucho las siguientes cabeceras (o algunas o ninguna; pero en ningún caso otra): Accept, Accept-language, Content-Language, Content-type.

  • El Content-Type está limitado a application/x-www-form-urlencoded, multipart/form-data y text/plain.

Si la petición que hemos hecho cumple con estos requisitos, el navegador solo buscará la cabecera Access-Control-Allow-Origin.

Solicitudes Verificadas. Qué es preflight

Es posible que la petición que estamos realizando no cumpla con las condiciones anteriormente citadas, por ejemplo:

  • Incluimos un Content-type no válido, como application/json.

  • Adjuntamos cookies o credenciales de algún tipo (muy común en apps)

  • Usamos un método como PATCH, PUT o DELETE

  • Varias de las anteriores al mismo tiempo.

En este caso, el navegador estima que es necesario añadir una capa más de seguridad y se pone en marcha el CORS menos amigable: el preflight. Esto significa que antes de realizar la propia petición, el navegador enviará al servidor web una petición alternativa del tipo OPTIONS para evaluar si es una llamada verificada. Es como si el navegador, antes de realizar la petición en sí, llamase al servidor de destino para asegurarse de que puede hacerlo con seguridad. Y no, el desarrollador no puede hacer nada aquí: es un proceso transparente que el navegador hace por nosotros.

En la solicitud de preflight, el navegador enviará al servidor una petición con las siguientes cabeceras (como dándole datos sobre lo que va a pasar y si lo permite):

  • Cabecera Origin: indica el origen actual; la web en la que estamos.

  • Cabecera Access-Control-Request-Method: qué método HTTP vamos a usar

  • Access-Control-Request-Headers: qué cabeceras vamos a adjuntar en la request además de las cabeceras automáticas y las básicas. Ejemplos son: authorization, x-yoquesé, etc.

En este caso, no se puede usar el comodín *. Hay que indicarlo específicamente.

El servidor web recibe esta petición del tipo OPTIONS y responde al navegador con las siguientes cabeceras:

  • Access-Control-Allow-Origin. Le indica qué origines están permitidos. Si coincide con el actual, genial; si no, para la conexión.

  • Access-Control-Allow-Methods: lista de métodos HTTP permitidos, como POST, PUT, GET ... Si el método coincide, seguimos adelante !

  • Access-Control-Allow-Headers: lista de cabeceras permitidas. Si las cabeceras que ibas a añadir a la petición están aquí, y el resto de cabeceras no dicen lo contrario, ¡ya puedes hacer la petición real!

Generalmente, el servidor determina si va a permitir la llamada basándose en la información que envía el navegador. Por ello, el servidor primero necesita que el cliente le contacte. Con dicha información del cliente, el servidor puede dinámicamente rechazar la conexión cambiando las cabeceras a valores no posibles.

Cabeceras de CORS de respuesta

CORS es un protocolo de seguridad que realizan los navegadores cuando una petición web tiene como url un origen diferente al actual. Si este es el caso, el navegador buscará las siguientes cabeceras en la respuesta del servidor web y detendrá la llamada si algo no es correcto.

Access-Control-Allow-Origin

El navegador busca esta cabecera en la respuesta y comprueba si la información que contiene dicha cabecera coincide con el origen actual (la web que estás visitando). En caso de coincidencia no hay problema; pero si no, detendrá la llamada y no la finalizará. Esto significa que es como si no se hubiese hecho.

Es decir, si en la cabecera figura como valor "Access-Control-Allow-Origin: app.es", la respuesta solo podrá leerse y procesarse si nos encontramos en dicha web. Si alguien hace una llamada desde otra web, no podremos leer la respuesta.

Esta cabecera permite el valor comodín asterisco "*", que significa que la respuesta es legible desde todos los orígenes. Una API (que puede ser llamada desde infinidad de orígenes utilizará * para no limitar desde donde se llama).

Access-Control-Allow-Origin: <origin> | *

Access-Control-Allow-Methods

Esta cabecera de respuesta indica qué métodos están permitidos. Por ejemplo, podemos indicar que solo podrán efectuarse llamadas usando GET y POST a un recurso en concreto (pero nunca PUT o PATCH). El navegador compara si el método HTTP que empleamos para llamar es uno de los que aquí figuran. De nuevo, si el método no está en la lista, la llamada será bloqueada.

Access-Control-Allow-Methods: <method>[, <method>]*

Solo se usa en respuestas verificadas.

Access-Control-Allow-Headers

Esta cabecera de respuesta indica qué cabeceras puede incluir la petición que originalmente hemos realizado. Si las cabeceras que hemos adjuntado en la petición no están presentes en Access-Control-Allow-Headers, la llamada será cancelada y no podrá ser procesada.

Hemos de notar que, además de las cabeceras que los navegadores automáticamente añaden, también se permiten las siguientes siempre (no es necesarias añadirlas a Access-Control-Allow-Headers):

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type

Es decir, si no declaramos ningún Access-Control-Allow-Headers y añadimos, por necesidad de nuestra plataforma la cabecera X-USER: 1, la conexión será interrumpida. Deberemos añadir en el servidor web la cabecera de respuesta "Access-Control-Allow-Headers: x-user".

Access-Control-Allow-Headers: <field-name>[, <field-name>]*

Solo se usa en respuestas verificadas.

Soluciones

Lamentablemente en producción, solo hay una solución válida: configurar tu servidor web (Apache, Nginx, etc.) para que sirva los recursos con la política same-site correcta. Así que te va a tocar pelearte con ello.

Pero si de momento estás haciendo algunas pruebas en local o en un grupo pequeño de usuarios (a los que puedes pedirles que hagan algún truco), es posible hacer algunos ajustes "temporales" o aplicar algunas soluciones para que CORS no te moleste en la fase de programación y pruebas (pero al final tendrás que corregirlo).

La primera pasa por poner la cabecera Access-Control-Allow-Origin: *. Como vimos antes, esto significa que cualquier origen podrá mostrar la respuesta. Es bastante inseguro, pero en desarrollo puedes permitírtelo.

La segunda, más sencilla si no quieres complicarte la vida, es bajarte una extensión para tu navegador que se encargue de puentear o desactivar CORS. Aquí ya depende del navegador que tengas y de la garantía o seguridad que te dé la extensión, pero en general cualquiera te valdría. Estas extensiones se encargan de que, cuando llega una petición, añaden o modifican las cabeceras CORS que son necesarias para saltar esta política de seguridad.

La tercera opción es utilizar un proxy. Este proxy se coloca entre el navegador y el destino final (la URL a donde quieres hacer la petición). Este se encarga de realizar la petición original al destino y devolverla al navegador (o bien con las cabeceras CORS modificadas o bien sin CORS al estar en el mismo dominio). Si por ejemplo estás usando React, puedes crear un proxy en el mismo dominio siguiente este manual.

Conclusión

A todos nos ha pasado que estamos desarrollando una web o una aplicación y CORS no ha dado algún problemilla. Y cuando no entiendes muy bien el porqué o el para qué, ese problemilla puede volverse un contratiempo.

Pero debes entender que CORS está ahí como política de seguridad, para salvaguardar la información del usuario y para intentar hacer de Internet un sitio un poco más seguro 🔐.

Configurar un servidor web como Apache o Nginx para que entregue los recursos con las cabeceras correctas puede ser un poco complicado la primera vez, pero una vez que pases por ese proceso, ya verás como es más sencillo la próxima vez.

Espero que hayas aprendido un poquito más sobre CORS, cómo funciona y cómo puedes hacer que no te cause muchos problemas.

Y también puedes aprender a configurar un servidor Nginx para que sea compatible con CORS, incluso en varios orígenes

¡Qué tengas un feliz coding!