Entender CORS

¡Vengadores, reuniros!

TL;DR: Para que una página web pueda hacer llamadas HTTP a una ruta situada en un dominio diferente al que se usó para descargarla debe pedir autorización primero al servidor del segundo dominio.

RSS para podcatcher

alojado en archive.org

Dale un retweet al post si depurando javascript alguna vez te has encontrado con el mensaje “Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource blah blah blah enabling CORS.”.

Vale, seguramente tienes una idea de lo que significa y de cómo se soluciona pero en el post de hoy vas a entender la brecha de seguridad que ayuda a evitar y qué es lo que pasa cuando activas CORS en el servidor para evitarlo. Empezamos.

Trivial

Lo primero: sí, significa Cross Origin Resource Sharing y está soportado everywhere.

Can I use CORS? frak yes.

¿Qué es un Cross Origin?

Imagina que Pepper Potts entra en su oficina en el penúltimo piso de la Torre Stark. Abre el portátil y se conecta a la wifi corporativa. Teclea intranet.avengers.com en la barra del navegador para acceder al portal de la empresa. Portal. Creo que todavía se dice así en algunos sitios.

El primer control de seguridad se realiza a nivel de red pero el bueno de JARVIS la deja pasar por utilizar la red interna así que Pepper se autentifica con su password (apuesto a que es bragasdeesparto) y en la pantalla aparece su homepage.

Potts a tope de Photoshop

Si no tienes muy claro cómo funciona este proceso puedes echar un vistazo a los posts sobre seguridad pero el detalle que tienes que recordar es que la autenticación ha creado una cookie en el navegador asociada a la sesión y cada vez que se acceda a una página bajo avengers.com se utilizará su valor para autorizar la petición. Y la Directora General de Industrias Stark está autorizada para casi todo.

Bueno, no tiene mucho trabajo pendiente: lee el par de avisos que aparecen en las notificaciones, aprueba dos pedidos de S.H.I.E.L.D que necesitan su visto bueno y rechaza la propuesta de Tony de comprar una destilería de whisky. Y obviamente una vez hecho esto entra en meneame.net porque le gusta estar al día de todo lo que se cuece alrededor del grafeno y los gatitos.

http://www.jennyparks.com/ironcat

Ahora imagínate que meneame está controlado en realidad por Hydra. Y que el post sobre grafeno contiene también un fichero javascript que abre una conexión a intranet.avengers.com, recupera datos confidenciales y los reenvía a meneame para disfrute del Barón Strucker. Dado que en principio Pepper ya se había autentificado y utiliza la red interna de los Vengadores podría parecer que todo está perdido ¿no?

Un script descargado desde meneame.net no puede crear conexiones a otro dominio sin pedir permiso previamente.

¡No! Porque precisamente para evitar este caso los navegadores implementan las políticas de Cross Origin Resource Sharing: un script descargado desde meneame.net no puede (por defecto) crear conexiones a otro dominio (como avengers.com) sin pedir permiso previamente.

Desgraciadamente esto supone también un problema para los vengadores: la documentación de armaduras de Iron Man utiliza gifs animados para hacer la lectura más interesante y estos se descargan desde un dominio llamado gatitos.avengers.com diferente al de la intranet.

Con las imágenes de los gatos no es problema: CORS está habilitado por defecto para este tipo de datos. Pero si lo que queremos es invocar el API para obtener un listado de urls de gatos en formato json ahí si se bloqueará la petición. A continuación te reproduzco las líneas de javascript problemáticas:

function recuperarListaGatitos(callback) {
  var request = new XMLHttpRequest();
  request.open('GET', 'https://gatitos.avengers.com/gifs');
  request.setRequestHeader('api-version', '2');
  request.onreadystatechange = callback;
  request.send();
}

Así que ¿cómo abrimos el paso al servicio de gatitos desde páginas alojadas en la intranet? Voy a contarte el caso completo y luego te explicaré que en muchas ocasiones todo es más directo y sencillo. Pero creo que de esta manera se entiende mejor.

Preflight y CORS

Te dejo un esquema y en las siguientes líneas lo explico con claridad, así que no te obsesiones demasiado todavía con él:

Esquema proporcionado por JARVIS

Básicamente cuando una página web trata de conectar con otro dominio el navegador intercepta esta petición y automáticamente añade otra request previa para obtener la autorización: es lo que llamamos el preflight, que consiste en utilizar el método HTTP OPTIONS con una serie de headers que informarán al servidor sobre la operación que quieres llevar a cabo. Las cabeceras que incluye el browser en la petición más importante son Origin con el dominio original (en nuestro caso, intranet.avengers.com) y Access-Control-Request-Method que indica el método que realmente se quiere llamar (GET, o el que sea).

El servidor de gatitos recibirá estos datos y está configurado para que al ser intranet.avengers.com un origen de confianza devuelva una serie de cabeceras al navegador pidiéndole que acepte realizar la petición real. Algo así:

Access-Control-Allow-Origin: http://intranet.avengers.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Max-Age: 18000

A partir de ese momento el navegador considerará durante cinco horas (18.000 segundos) que cualquier petición POST, GET u OPTIONS generada desde un script descargado desde intranet.avengers.com tiene permiso para invocar rutas en el dominio de los gatitos.

La petición real hecha por el javascript que te he enseñado antes se ejecutará automáticamente en este momento. Solo si no se reciben estas cabeceras el navegador opta mostrar el error del que hablábamos al principio del post. Y obviamente si Access-Control-Allow-Origin es meneame.net nuestro servidor de gatitos denegará la autorización.

¡Ah! Si el API de gatitos fuese pública (accesible para todo el mundo) en lugar de retornar en el Access-Control-Allow-Origin un dominio concreto la respuesta incluiría un simple * indicando que no nos importa de dónde venga la petición cross-origin.

Así que sí: has estado utilizando el método OPTIONS durante años probablemente sin saberlo, cada vez que una página usaba un api con un endpoint situado en un dominio distinto al que la alojaba.

¿Cuándo es innecesario el preflight?

Como te decía hace un rato este es el escenario más sofisticado pero en muchos casos ni siquiera se producirá un preflight. Tienes que cumplir tres condiciones:

  • Que el método sea GET, HEAD o POST
  • Que solo se envíen los headers Accept, Accept-Language, Content-Language o Content-Type
  • Que la codificación sea application/x-www-form-urlencoded, multipart/form-data o text/plain

Si pones el check en los tres puntos la petición se envía directamente al servidor sin utilizar un OPTIONS previo y el server se limita a retornarte un Access-Control-Allow-Origin si todo está bajo control.

Cómo habilitar CORS

Ahora imagina que eres el o la responsable de administrar el API de gatitos. ¿Cómo activamos el soporte para CORS? Pues te lo puedes imaginar: intercepta las llamadas, revisa las cabeceras que hemos visto antes e implementa la respuesta correspondiente.

Si utilizas el framework SpringBoot aquí tienes el código necesario para activar CORS en tu API. Si utilizas cualquier otro framework o servidor casi seguro que encontrarás la manera de configurarlo en enable-cors.org, un site súper recomendable para guardarlo como referencia.

¿Crees que el código javascript que hemos visto antes provocaría un preflight o cumple los requerimientos que hemos visto antes para evitar esa llamada extra?

JSON-p como alternativa a CORS

Admitámoslo: a veces hasta los Vengadores tienen que caer en chapuzas para solucionar la papeleta. Porque lo de cargar la armadura de Iron Man con una batería de coche ya me dirás. En nuestro caso va a ser utilizar javascript en lugar de JSON para evitar las restricciones CORS.

La jugada es la siguiente: como ya sabes una página web se representa como una serie de estructuras de datos en memoria del navegador llamado el DOM. Por ejemplo, un HTMLParagraphElement sirve para guardar la información de un párrafo. Esas estructuras de datos son dinámicas: puedes manipularlas o crearlas cuando quieras para modificar la pantalla que el usuario está viendo.

Bueno, pues nuestra página que necesita invocar al servidor siempre puede crear un elemento de tipo HTMLScriptElement que representa el clásico <script></script> web con la url del API. Al añadirlo a la página el navegador invocará esa dirección, descargará lo que allí se encuentre y lo evaluará como javascript ¡sin políticas CORS restrictivas!

Aquí tenemos dos problemas obvios: el primero es que si en algún momento el servidor envía código javascript malicioso el navegador lo ejecutará. El segundo es que el API no puede retornar un simple objeto JSON. Veamos un ejemplo:

[ "/gifs/gato-1.jpg", "/gifs/gato-2.jpg", "/gifs/gato-2.jpg"]

Si una página carga este documento como un script se creará un array con tres cadenas de caracteres en memoria… pero no tendremos forma de acceder a él porque no se guarda en ninguna variable. Y aquí es donde interviene el padding (la p de JSON-p) y que es una forma elegante de explicar que hay que añadir cosas a la izquierda y a la derecha de los datos que nos interesan para que se mantenga la sintaxis correctamente pero el resultado sea manipulable.

Típicamente lo que retornaría el API de gatitos es una llamada a una función pasando como parámetros los datos. Por ejemplo:

callback([ '/gifs/gato-1.jpg', '/gifs/gato-2.jpg', '/gifs/gato-2.jpg'])

Ahora lo que se descarga ya no es una estructura de datos si no la llamada a una rutina (que arbitrariamente tiene el nombre callback en este caso) que recibirá los datos que nos interesan. Obviamente el código de esa rutina tiene que encargarse de procesar la respuesta.

Cualquier librería que se precie permite trabajar automáticamente con JSON-p de una forma muy cómoda pero con diferencias sutiles respecto a XMLHTTPRequest. La más importante es que no hay una forma sencilla de saber que la invocación ha fracasado porque la llamada al servidor está desconectada del código de la página que la quiere llevar a cabo (es el elemento <script> quien la gestiona). Tenlo en cuenta si la conexión no es fiable.

Recursos adicionales

Lo sé, solo me ha llevado 22 artículos recordar que tengo que poner la bibliografía útil. Pero más vale tarde que nunca, así que empezamos en este:

  • enable-cors.org para ver rápidamente cómo configurar tu servidor.
  • MDN CORS porque si hay un sitio en el que estas cosas estén mejor explicadas que aquí es la MDN.
  • HTML5 Using CORS porque es un clásico.

En el siguiente capítulo

Que ya te digo que no será la próxima semana porque sigo de vacaciones en Menorca y difícilmente voy a encontrar tiempo para hacerlo. Pero en 15 días subiré un vídeo demostrándote lo que hemos estado viendo en este post ¿de acuerdo? ¡Tienes suerte de venir del futuro porque ya está publicado el vídeo sobre cómo configurar CORS en Apache !

Nos vemos dentro de nada en… programar.cloud ;-)

jv

pd: La música que te hace sonreír es de Marcus y la ilustración del post ha salido de la wikipedia. Yo quiero salir de fiesta con esta gente.

ppd: Que noooooo, que es broma: que Varsavsky no es un supervillano. Además tiene casa en Menorca, decidle que si necesita amo de llaves aquí estoy.

pppd: Ves a ver Wonder Woman ya.

ppppd: Puedes encontrar un montón de gatitos vengadores en JennyParks.com.