Entendiendo CORS y aplicando soluciones

Publicado por José Francisco Díaz López el

FrontDesarrollo WebDesarrollo AplicacionesSeguridad

En el desarrollo moderno de aplicaciones es común encontrarse con el problema del CORS. Pese a ser tan común, es muy desconocido, por ello primero vamos a aprender de que se trata esto del CORS, por qué ocurre, y a continuación vamos a ver cómo solucionar estos problemas.

Cross origin resource sharing (CORS) es un mecanismo basado en cabeceras http que permite a los servidores indicar a los navegadores si deben permitir la carga de recursos para un origen distinto al suyo (dominio, esquema o puerto).

Un ejemplo sencillo que os sonará muy común:

Desde el dominio:

https://mi-aplicacion  

Realizamos las llamadas para obtener datos a:

https://mi-aplicacion-api  

Para resolver el problema primero vamos a comprender en qué consiste, para luego aplicar las soluciones correctas en función del caso de uso, que en ningún caso puede ser deshabilitar la seguridad del navegador.
El sistema CORS funciona añadiendo nuevas cabeceras http que permiten a los servidores especificar a los navegadores desde qué orígenes se puede acceder.

Además, si la llamada puede tener efectos colaterales o maneja información sensible, como pueden ser datos de usuarios, cabeceras custom o datos de cookies, se obliga a hacer una llamada "preflight".

Esta llamada preflight consiste en una petición http OPTIONS, en la cual el navegador espera obtener el "permiso" para realizar la llamada final, esta llamada la explicaremos con detenimiento más adelante.

En caso de no activarse el preflight, se realizará una llamada CORS simple, la cual tiene unas normas más sencillas de satisfacer.

Como detalle importante, los errores por CORS no se pueden analizar desde javascript, solo sabremos que ha habido un error, pero no sabremos nada mas, para identificar estos errores deberemos mirar en la consola de desarrollo de los navegadores, en la cual sí podremos ver estos errores.

Os suena, ¿verdad?

¿Qué llamadas usan CORS?

  • Llamadas XMLHttpRequest y fetch
  • Web Fonts (@font-face uso en diferente dominio en css)
  • Video e imágenes insertadas via drawImage()
  • Texturas WebGL
  • Texturas css de imágenes

Llamadas CORS simples

El navegador no siempre necesita una llamada preflight. En este caso realiza una llamada CORS simple, en la que el navegador únicamente espera de vuelta una cabecera en la respuesta (Access-Control-Allow-Origin), este tipo de llamadas deben de satisfacer obligatoriamente los siguientes requisitos:

  • Métodos permitidos: HEAD, GET, POST
  • Solo puede contener las cabeceras añadidas por el navegador, y las siguientes que pueden ser introducidas programáticamente: Accept, Accept-language, Content-Language, Content-type (con las restricciones del punto siguiente)
  • La cabecera content-type estará restringida a los siguientes valores: application/x-www-form-urlencoded, multipart/form-data, text/plain.
  • No tener un listener a upload de la llamada XMLHttpRequest.
  • No usar un objeto ReadableStream en la respuesta.

En la respuesta de esta llamada el servidor deberá añadir la cabecera:

Access-Control-Allow-Origin  

Esta cabecera idealmente deberá contener el valor de origin de la petición, en nuestro ejemplo:

Access-Control-Allow-Origin: https://mi-aplicacion  

En este caso simple se permite usar el wildcard * en la respuesta del servidor, según el cual especificamos que cualquiera puede acceder a los recursos del servidor.

Access-Control-Allow-Origin: *  

Recordar siempre analizar las implicaciones en tema de seguridad con el uso de los wildcards.

A continuación se detalla el flujo en las llamadas CORS simples:

Llamadas con preflight

Si el navegador estima que no es seguro realizar una llamada CORS simple, este realizará el proceso de preflight, mediante el cual lanzará una llamada de tipo OPTIONS para evaluar si la llamada CORS es segura.

Esta llamada se realiza automáticamente por el navegador, sin intermediación por parte del programador. Este  no puede evitarla de ninguna manera.

Ejemplos de llamadas con preflight:

  • content-type no válido para llamadas simples, por ejemplo, application/json. En este caso deberá añadirse content-type en la respuesta de servidor como cabecera permitida
  • credenciales, cookies
  • cabeceras no permitidas, authentification, cabecera-personalizada, etc
  • Llamadas a métodos como PATCH, PUT, DELETE
  • Cualquier combinación no establecida para las llamadas simples a dominios diferentes al origen de nuestra web.

Las cabeceras a establecer son mayores, y no podremos usar el wildcard "*" en las respuestas en el caso de activarse las credenciales para el envío de cookies.

El navegador incluirá automáticamente las siguientes cabeceras en la petición OPTIONS:

  • Origin: origen de nuestra página web
  • Access-Control-Request-Method: método el cual queremos ejecutar
  • Access-Control-Request-Headers: cabeceras adicionales que queremos usar,por ejemplo:  authentification, authorization, o cualquier cabecera añadida no incluida en las soportadas para cors simple. incluirá content-type en caso de llevar un tipo no incluido en los simples.
  • Cookie: Si en la llamada XMLHttpRequest o fetch especificamos withCredentials/credentials se incluirán cookies, por defecto se omiten
no podremos usar el wildcard "*" en las respuestas en el caso de habilitar las credenciales para el envío de cookies

Nuestro servidor puede hacer uso de estas cabeceras para establecer dinámicamente las cabeceras necesarias.

Y el servidor deberá responder con las siguientes cabeceras:

  • Access-Control-Allow-Origin: dominio desde donde se pueden realizar llamadas, en nuestro ejemplo: https://mi-aplicacion
  • Access-Control-Allow-Methods: Lista de metodos permitidos en el servidor para el recurso solicitado, ejemplo: HEAD, PUT, PATCH, POST,...
  • Access-Control-Allow-Headers: headers añadidos mediante javascript permitidos, caso de tener un content-type no permitido en la versión simple de CORS debe ser incluido, ejemplo: content-type, authentification, authorization, o cualquier cabecera añadida no incluida en las soportadas para CORS simple.
  • Access-Control-Max-Age: valor en segundos para la cual la llamada preflight es válida. Es decir, el navegador no emitirá este control hasta que expire el tiempo. Hay que tener en cuenta los máximos de los navegadores, los cuales tienen preferencia, 2 horas para derivados de chromium, 24 horas en mozilla firefox. Un valor de -1 obliga al navegador a realizar las llamadas preflight para cada llamada CORS.

En caso de que el servidor incluya el valor del origen dinámicamente, debe incluir también la cabecera: vary: Origin indicando que la respuesta puede variar en función del origen.

Ejemplo de flujo de llamadas CORS con preflight:

Soluciones

La primera solución es implementar un sistema de cabeceras adecuado en los servicios de backend, esto no quiere decir que los desarrolladores front no tengamos nada que aportar. Deberemos ayudar a definir una correcta estrategia de cabeceras, evitando los wildcards y las soluciones "fáciles", y definiendo las cabeceras correctas en función de los entornos.

También es posible habilitar un reverse proxy, via nginx o similar, en el cual se registren los patrones de las llamadas a los servicios y se redireccionen hacia estos. De esta manera todo está bajo un mismo dominio y evitamos toda la problemática. Existen numerosas soluciones ya en internet, dejamos link de una de las numerosas soluciones. No obstante esta solución no es siempre posible.

Podemos tener el caso en el que aunque los servicios tengan habilitadas las cabeceras correctamente para los entornos, cuando desarrollamos nuestro frontal en local surge este error. Una solución es incluir el dominio en nuestro fichero host del sistema apuntando a nuestro local. Mediante la modificación de este fichero le indicamos a nuestro sistema que cuando el navegador pida ese dominio se redirija a dónde especifiquemos.

No obstante hay casos en los que definir estas cabeceras puede ser innecesario. Por ejemplo, en un entorno de producción, tanto api como frontales se encuentran en el mismo dominio, solo en desarrollo ocurre este fallo de CORS, donde al levantar nuestros frontales desde nuestros entornos de desarrollo se produce el temido error de CORS.

De manera que se hace necesaria una solución especifica para los frontales. Cabe destacar que usar estas soluciones para algo que no sea desarrollo es muy peligroso y está completamente desaconsejado.

En estos casos es bastante normal que los propios frameworks/librerias ya dispongan de herramientas para mitigar estos errores. Esta solución consiste en añadir un servidor a modo de proxy, el cual hace las llamadas al backend real, y devuelve las llamadas a nuestros frontales.

Es muy importante no usar estas soluciones directamente sin comentar el problema de CORS con el equipo, ya que podemos estar escondiendo un problema, que surgirá al cambiar de entorno. El problema de CORS es un problema sencillo si se identifica rápido, en caso contrario puede ser problemático.

A continuación os añadimos un ejemplo de uso para los principales frameworks:

Angular (>= 2)

https://angular.io/guide/build

Creamos un fichero proxy.conf.json en la raíz de nuestro proyecto

A continuación indicamos a Angular que debemos usarlo. Puede ser de las siguientes maneras:

A traves del CLI:

ng-serve --proxy-config proxy.conf.json  

En package.json:

"scripts": {
    "start": "ng serve --proxy-config proxy.conf.json"
}

En el proxyConfig de nuestro fichero angular.json

... 
"architect": { 
    "serve": {
        "builder": "@angular-devkit/build-angular:dev-server", 
        "options": { 
            "browserTarget": "your-application-name:build", 
            "proxyConfig": "proxy.conf.json" 
        },
...

O si nuestro fichero es angular-cli.json:

... 
"defaults": { 
    "styleExt": "sass", 
    "component": {}, 
    "serve": { 
        "proxyConfig": "proxy.conf.json" 
    } 
}
...

Y dentro de nuestro fichero proxy.conf.json:

{ 
    "/api": {
        "target": "http://localhost:3000",
        "secure": false,
        "pathRewrite": {
            "^/api": ""
        }
    }
}

Estamos especificamos que todas las llamadas realizadas a /api serán interceptadas por el proxy, enviadas a http://localhost:3000 de forma no segura y eliminando la parte de "api" de la llamada (/api/user se convertiría en /user).

En caso de estar dentro de un proxy corporativo y que la llamada no es a localhost es posible que esta solución no valga por sí misma, y se necesario usar https-proxy-agent.

React, create react app

En el caso de usar create react app tenemos dos opciones muy sencillas:

Primera opción, añadir un campo en package.json:

"proxy": "http://localhost:4000"

Con esto, al realizar las llamadas a /api/user, el sistema las redirigirá a http://localhost:4000/api/user

Segunda opción, usando http-proxy-middleware, requiere cra >= 2.0.0

npm install http-proxy-middleware --save  
o  
yarn add http-proxy-middleware  

Creamos un fichero dentro de src: setupProxy.js

Podemos usar el siguiente contenido de base:

const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function(app) {  
    app.use(
        '/api',
        createProxyMiddleware({
            target: 'http://localhost:4000',
            changeOrigin: true,
        })
    );
};

Con esto al realizar las llamadas a /api/user, el sistema las redirigirá a http://localhost:4000/api/user)

Vue.js

En este caso también es sumamente sencillo

Para ello en nuestro fichero vue.config.js configuramos en el apartado devServer

module.exports = {  
  devServer: {
    proxy: {
      '^/api': {
        target: 'http://localhost: 4000',
        ws: true,
        changeOrigin: true
      }
    }
  }
}

De manera que las llamadas que comienzan por api, son redirigidas a localhost:4000

Independiente de la librería/framework

Otro modo sería crearnos nuestro servidor proxy en el cual inyectar las cabeceras. Existen numerosas soluciones, a modo de ejemplo añadimos cors anywhere o local cors proxy.

A continuación un ejemplo funcional sacado de la propia documentación de cors anywhere:

// Listen on a specific host via the HOST environment variable
var host = process.env.HOST || '0.0.0.0';  
// Listen on a specific port via the PORT environment variable
var port = process.env.PORT || 8080;

var cors_proxy = require('cors-anywhere');  
cors_proxy.createServer({  
    originWhitelist: [], // Allow all origins
    requireHeader: ['origin', 'x-requested-with'],
    removeHeaders: ['cookie', 'cookie2']
}).listen(port, host, function() {
    console.log('Running CORS Anywhere on ' + host + ':' + port);
});

Al realizar una llamada a http://localhost:8080/mi-aplicacion-api:4000/api/users

devolverá la llamada con las cabeceras correctas, así como el comportamiento esperado frente al preflight.

Con esto esperamos haberos ayudado a comprender el funcionamiento de CORS, y que este no sea más un problema en vuestros desarrollos.

Si te ha gustado, ¡síguenos en Twitter para estar informado de próximas entregas!

Un saludo y happy coding