Nuestros esfuerzos en materia de conectividad se centran en ampliar el acceso y la adopción de Internet en todo el mundo. Esto incluye nuestro trabajo en tecnologías como Terragraph, nuestra colaboración con los operadores de telefonía móvil en los esfuerzos para ampliar el acceso rural, nuestro trabajo como parte del Proyecto Telecom Infra, y programas como Free Basics. A medida que hemos ido trabajando en Free Basics, hemos escuchado las opiniones y recomendaciones de la sociedad civil y otras partes interesadas. Hemos desarrollado Discover específicamente para abordar e incorporar esas recomendaciones en un nuevo producto que apoya la conectividad. Hoy, Facebook Connectivity y nuestros socios de Bitel, Claro, Entel y Movistar están lanzando una prueba de Discover en Perú.

Proporcionar este servicio y al mismo tiempo mantener a la gente a salvo de posibles riesgos de seguridad fue un duro desafío técnico. Queríamos desarrollar un modelo que nos permitiera presentar de forma segura las páginas web de todos los dominios disponibles, incluyendo sus recursos (scripts, medios, hojas de estilo, etc.). A continuación, repasamos el modelo que construimos, las elecciones de arquitectura únicas que hicimos en el camino y los pasos que hemos dado para mitigar los riesgos.

Donde empezamos

Para Free Basics, nuestro reto era encontrar una forma de proporcionar un servicio sin coste a las personas que utilizan la web móvil, incluso en teléfonos con características sin soporte de aplicaciones de terceros. Los socios de las operadoras de telefonía móvil podían prestar el servicio, pero las limitaciones de la red y de los equipos de las pasarelas hacían que sólo pudiera ser gratuito el tráfico a determinados destinos (normalmente rangos de direcciones IP o una lista de nombres de dominio). Con más de 100 socios en todo el mundo y el tiempo y la dificultad que suponía cambiar las configuraciones de los equipos de red de los operadores, nos dimos cuenta de que teníamos que idear un nuevo enfoque.

Ese nuevo enfoque requería que primero construyéramos un servicio proxy basado en la web en el que el operador pudiera poner el servicio a disposición de forma gratuita en un único dominio: freebasics.com. A partir de ahí, buscábamos las páginas web en nombre del usuario y las entregábamos a su dispositivo. Incluso en los navegadores modernos, las arquitecturas proxy basadas en la web plantean algunos problemas. En la web, los clientes pueden evaluar las cabeceras HTTP de seguridad, como el uso compartido de recursos entre orígenes (CORS) y la política de seguridad de contenidos (CSP), y hacer uso de las cookies directamente desde el sitio. Pero en una configuración de servidor proxy, el cliente interactúa con el proxy, y éste actúa como cliente del sitio. El proxy de sitios web de terceros a través de un único espacio de nombres viola algunas suposiciones sobre cómo se almacenan las cookies, cuánto acceso tienen los scripts para leer o editar el contenido, y cómo se evalúan CORS y CSP.

Para abordar estas preocupaciones, inicialmente impusimos algunas limitaciones directas, incluyendo qué sitios se podían visitar con Free Basics y la imposibilidad de ejecutar scripts. Esto último se ha convertido en un problema más importante con el paso del tiempo, ya que muchos sitios web, incluidos los de móviles, han empezado a depender de JavaScript para funciones críticas, como la representación de contenidos.

Arquitectura inicial

Diseño del dominio

Para dar cabida a la funcionalidad limitada de muchas pasarelas de operadores de móviles, consideramos arquitecturas alternativas, como:

  1. Una solución cooperativa en la que los sitios web pueden asignar un subdominio (por ejemplo, free.example.com) y resolverlo en nuestro espacio IP para que los operadores lo hagan gratuito para el usuario.

Esta solución tenía pros:

  • Permitía la comunicación directa de extremo a extremo entre el cliente y el servidor.
  • Requería una intervención mínima en el lado del proxy.

Sin embargo, también tenía algunas desventajas:

  • Los sitios tenían que optar por este esquema, lo que suponía costes de ingeniería adicionales para los propietarios de los sitios.
  • Los navegadores tendrían que solicitar un dominio específico a través de la Indicación de Nombre de Servidor (SNI), para que el proxy supiera dónde conectarse. Sin embargo, el soporte para SNI no es universal, lo que hace que esta solución sea menos viable.
  • Si los abonados navegaran accidentalmente a example.com directamente, en lugar de al subdominio free.example.com, incurrirían en cargos – y no necesariamente serían redirigidos al subdominio a menos que el operador hubiera implementado alguna lógica extra.
  1. La encapsulación IPv4 en IPv6, donde podemos encapsular todo el espacio IPv4 dentro de una única subred IPv6 de datos libres. Un resolver DNS personalizado entonces resuelve IPv4 recursivamente y responde con respuestas IPv6 encapsuladas.

Esta solución también tenía pros:

  • No requería la cooperación del propietario del sitio web.
  • No había necesidad de SNI para resolver la IP remota.

Y los contras:

  • Los navegadores verían el dominio www.example.com.freebasics.com, pero el certificado www.example.com daría lugar a un error.
  • Sólo unas pocas pasarelas de portadores soportaban IPv6 de esta manera.
  • Aún menos dispositivos soportaban IPv6, especialmente las versiones más antiguas del sistema operativo.

Ninguna de estas era una solución viable. Finalmente, decidimos que la mejor arquitectura posible sería el colapso de origen, donde nuestro proxy se ejecuta dentro de un único espacio de nombres de dominio colapsado de origen bajo freebasics.com. Los operadores pueden entonces permitir el tráfico a este destino más fácilmente y mantener sus configuraciones simples. Cada origen de terceros está codificado en un subdominio, por lo que podemos garantizar que la resolución de nombres siempre dirigirá el tráfico a una IP libre.

Por ejemplo:

https://example.com/path/?query=value#anchor

Se reescribe a:

https://https-example-com.0.freebasics.com/path/?query=value#anchor

Hay una amplia lógica del lado del servidor para asegurar que los enlaces y las referencias se transforman correctamente. Esta misma lógica ayuda a garantizar que incluso los sitios sólo HTTP se entreguen de forma segura a través de HTTPS en Free Basics entre el cliente y el proxy. Este esquema de reescritura de URLs nos permite utilizar un único espacio de nombres y certificado TLS, en lugar de requerir un certificado separado para cada subdominio en Internet.

Todos los orígenes de Internet se convierten en hermanos bajo 0.freebasics.com, lo que plantea ciertas consideraciones de seguridad. No pudimos aprovechar la ventaja de añadir el dominio a la lista de sufijos públicos, ya que tendríamos que emitir una cookie diferente para cada origen, lo que acabaría superando los límites de cookies de los navegadores.

Cookies

A diferencia de los clientes web, que pueden hacer uso de las cookies directamente desde el sitio, el servicio proxy requiere una configuración diferente. Free Basics almacena las cookies del usuario en el lado del servidor por varias razones:

  1. Los navegadores móviles de bajo nivel suelen tener un soporte limitado de cookies. Si emitimos incluso sólo una cookie por sitio bajo nuestro dominio proxy, podríamos estar limitados a establecer sólo decenas de cookies. Si Free Basics estableciera cookies del lado del cliente para cada sitio bajo 0.freebasics.com, los navegadores más antiguos alcanzarían rápidamente los límites de almacenamiento de cookies locales – e incluso los navegadores modernos alcanzarían un límite por dominio.
  2. Las restricciones del espacio de nombres del dominio que necesitábamos implementar también excluían el uso de cookies hermanas y jerárquicas. Por ejemplo, una cookie establecida en cualquier subdominio en .example.com normalmente sería legible en cualquier otro subdominio. En otras palabras, si a.example.com establece una cookie en .example.com, entonces b.example.com debería poder leerla. En el caso de Free Basics, a-example-com.0.freebasics.com establecería una cookie en example.com.0.freebasics.com, lo que no está permitido por la norma. Como eso no funciona, otros orígenes, como b-example-com.0.freebasics.com, no podrían acceder a las cookies establecidas para su dominio principal.

Para permitir que el servicio proxy acceda a este tarro de cookies del lado del servidor, Free Basics aprovecha dos cookies del lado del cliente:

  1. La cookie datr, un identificador del navegador utilizado con fines de integridad del sitio.
  2. La ick (clave de cookie de Internet), que contiene una clave criptográfica utilizada para cifrar el tarro de cookies del lado del servidor. Como esta clave se almacena sólo en el lado del cliente, el tarro de cookies del lado del servidor no puede ser descifrado por Free Basics cuando el usuario no está utilizando el servicio.

Para ayudar a proteger la privacidad y la seguridad del usuario cuando almacena sus cookies en un tarro de cookies del lado del servidor, nos aseguramos de que:

  1. Las cookies del lado del servidor se cifran con una ick que se guarda sólo en el cliente.
  2. Cuando el cliente proporciona la ick, ésta es olvidada por el servidor en cada petición sin llegar a ser registrada.
  3. Marcamos las dos cookies del lado del cliente como Secure y HttpOnly.
  4. Hacemos un hash del índice de una cookie utilizando la clave del lado del cliente para que la cookie no sea rastreable hasta el usuario cuando la clave no está presente.

Permitir la ejecución de scripts supone un riesgo de fijación de las cookies del lado del servidor. Para evitarlo, excluimos el uso de JavaScript de Free Basics. Además, aunque cualquier sitio web puede formar parte de Free Basics, revisamos cada sitio individualmente en busca de posibles vectores de abuso, independientemente del contenido.

Mejorando lo que habíamos construido

Para apoyar un modelo que sirva a cualquier sitio web, con la capacidad de ejecutar scripts de forma más segura, tuvimos que replantear significativamente nuestra arquitectura para evitar amenazas, como que los scripts puedan leer o fijar las cookies del usuario. JavaScript es extremadamente difícil de analizar y evitar que se ejecute un código no deseado.

Como ejemplo, he aquí algunas formas en las que un atacante podría inyectar código que necesitaríamos poder filtrar:

setTimeout();location = ' javascript:alert(1) <!--';location = 'javascript\n:alert(1) <!--';location = '\x01javascript:alert(1) <!--';var location = 'javascript:alert(1)';for(location in {'javascript:alert(1)':0}); = 'javascript:alert(1)';location.totally_not_assign=location.assign;location.totally_not_assign('javascript:alert(1)');location] = 'javascript:alert(1)';Reflect.set(location, 'href', 'javascript:alert(1)')new Proxy(location, {}).href = 'javascript:alert(1)'Object.assign(window, {location: 'javascript:alert(1)'});Object.assign(location, {href: 'javascript:alert(1)'});location.hash = '#%0a alert(1)';location.protocol = 'javascript:';

El modelo que ideamos amplió el diseño de Free Basics, pero también protege la cookie que almacena la clave de cifrado para que no sea sobrescrita por los scripts. Usamos un marco exterior en el que confiamos para atestiguar que el marco interior, que presenta contenidos de terceros, no está siendo manipulado. La siguiente sección muestra en detalle cómo mitigamos la fijación de la sesión y otros ataques, como el phishing y el clickjacking. Exponemos un método para servir de forma segura el contenido de terceros mientras se permite la ejecución de JavaScript.

Mejoras en la arquitectura de Discover

Las referencias al dominio en este punto cambiarán a nuestro nuevo dominio, un discoverapp.com de origen similar.

JavaScript y fijación de cookies

Al permitir JavaScript desde sitios de terceros, hemos tenido que reconocer que esto permite ciertos vectores para los que debíamos prepararnos, ya que los scripts pueden modificar y reescribir enlaces, acceder a cualquier parte del DOM y, en el peor de los casos, fijar las cookies del lado del cliente.

La solución que se nos ocurrió tenía que hacer frente a la fijación de cookies, así que en lugar de intentar analizar y bloquear ciertas llamadas de scripts, decidimos detectarlo en el momento en que se produce y hacerlo inútil. Esto se consigue de la siguiente manera:

  1. Al registrarse, generamos una nueva y segura ick aleatoria.
  2. Enviamos ick al navegador como una cookie HttpOnly.
  3. Entonces hacemos un HMAC de un valor llamado ickt a partir de un compendio tanto de ick como de datr (para evitar la fijación de ambos) y almacenamos una copia de ickt en el cliente, en una ubicación en localStorage en la que un potencial atacante no pueda escribir. La ubicación que utilizamos es https://www.0.discoverapp.com, que nunca sirve contenido de terceros. Dado que este origen es hermano de todos los orígenes de terceros, no puede producirse una bajada de dominio o cualquier otro tipo de modificación del mismo, y el origen se considera de confianza.
  4. Incorporamos ickt, derivado de la cookie ick vista en la petición, dentro del HTML en cada respuesta de proxy de terceros.
  5. Cuando se carga la página, comparamos la ickt incrustada con la ickt de confianza utilizando window.postMessage(), e invalidamos la sesión si hay una discrepancia eliminando las cookies datr y ick.
  6. Impedimos la interacción del usuario con la página hasta que este proceso se complete.

Como protección adicional, establecemos una nueva cookie datr si detectamos múltiples cookies en la misma ubicación, incrustando una marca de tiempo para que siempre podamos usar la más reciente.

Solución de dos marcos

Para la validación, necesitamos una forma para que una página de terceros consulte el valor ickt y lo valide. Hacemos esto incrustando el sitio de terceros dentro de un <iframe> en una página en el origen seguro e inyectando una pieza de JavaScript en el sitio de terceros. Construimos un marco exterior seguro y un marco interior de terceros.

Marco interior

Dentro del marco interior, inyectamos un script en cada página proxy que servimos. También inyectamos el valor ickt calculado a partir del ick visto en la petición junto con él. El comportamiento del marco interno es el siguiente:

  1. Comprueba con el marco externo:
    • postMessage a la parte superior con ickt incrustado en la página.
    • Espera.
    • Si el script obtiene un reconocimiento del origen seguro, dejamos que el usuario interactúe con la página.
    • Si el script espera demasiado tiempo o recibe una respuesta de un origen inesperado, navegaremos el marco a una pantalla de error sin contenido de terceros (nuestra página «Oops»), porque es posible que el marco exterior no esté o sea diferente de lo que el marco interior espera.
  2. Comprueba con parent:
    • postMessage a parent.
    • Espera.
    • Si el script obtiene una respuesta con source===parent y origen bajo .0.discoverapp.com, procederá.
    • Si el script espera demasiado, o recibe una respuesta de un origen inesperado, navegaremos a la página «Oops».

Algunas notas sobre el marco interno:

  1. Incluso si se sortea, los atacantes potenciales serían capaces de fijarse sólo en un origen en el que puedan lograr la ejecución de código, haciendo que los vectores de fijación de cookies sean redundantes.
  2. Suponemos que un origen benigno no eludirá deliberadamente el protocolo de mensajería entre el interior y el exterior.

Marco exterior

El marco exterior está ahí para atestiguar que el marco interior es consistente:

  1. Nos aseguramos de que el marco exterior sea siempre el marco superior con JavaScript y X-Frame-Options: DENY.
  2. Espera a postMessage.
  3. Si el marco exterior recibe un mensaje:
    • ¿Es de un origen del marco interior?
    • Si es así, ¿informa del valor correcto de ickt?
      • Si es así, envía un mensaje de acuse de recibo.
      • Si es no, elimina la sesión, borra todas las cookies y navega a un origen seguro.
  4. Si el marco exterior no recibe un mensaje durante unos segundos o el subcuadro no es el marco interior más alto, eliminamos la ubicación de la barra de direcciones del marco seguro.

Interacción con la página

Para evitar condiciones de carrera en las que una persona podría introducir una contraseña bajo una cookie fijada antes de que el marco interior haya completado la verificación, es importante evitar que la gente interactúe con la página antes de que se complete la secuencia de verificación del marco interior.

Para evitar esto, el servidor añade style="display:none" al elemento <html> de cada página. El marco interno lo eliminará cuando obtenga la confirmación del marco externo.

Se sigue permitiendo la ejecución de código JavaScript y se siguen obteniendo recursos. Pero mientras la persona no haya introducido ninguna entrada en la página, el navegador no hace nada que un atacante potencial no pudiera haber hecho simplemente visitando el sitio – a menos que el sitio ya sea vulnerable a la falsificación de petición de sitio cruzado (CSRF).

Al optar por esta solución, tuvimos que resolver otros posibles resultados, específicamente:

  1. Fijación de cookie asíncrona.
  2. Clickjacking debido a framing.
  3. Phishing suplantando el dominio Discover.

Fijación asíncrona de cookies

Hasta este punto, las protecciones que hemos implementado han tenido en cuenta las fijaciones síncronas, pero también pueden ocurrir asíncronamente. Para evitarlo, utilizamos un método clásico de prevención de CSRF. Requerimos que los POSTs lleven un parámetro de consulta con el datr visto al cargar la página. Luego comparamos el parámetro de consulta con la cookie datr vista en la solicitud. Si no coinciden, no atendemos la petición.

Para evitar la filtración de datr, incrustamos una versión encriptada del datr dentro del marco interno y nos aseguramos de que este parámetro de consulta se añada a cada objeto <form> y XHR. Como la página no puede derivar el token datr por sí misma, el datr añadido es el que se ve en ese momento.

Para las peticiones anónimas, requerimos que también tengan el parámetro de consulta datr. El anonimato se preserva porque no lo filtramos al sitio de terceros: falta la cookie ick, por lo que no podemos usar el tarro de cookies. Sin embargo, en este caso, no somos capaces de validar contra la cookie datr, por lo que los POSTs anónimos se pueden hacer bajo sesiones fijas. Pero como son anónimos y carecen de la ick, no se puede filtrar información sensible.

Clickjacking

Cuando un sitio envía X-Frame-Options: DENY, no se cargará en un marco interno. Esta cabecera es utilizada por los sitios web para evitar la exposición a ciertos tipos de ataques, como el clickjacking. Eliminamos esa cabecera de la respuesta HTTP pero pedimos al marco interno que verifique que parent es el marco de la ventana top utilizando postMessage. Si la validación falla, navegamos al usuario a la página «Oops».

Phishing

La «barra de direcciones» que proporcionamos en el marco seguro se utiliza para exponer el origen del marco interno superior al usuario. Sin embargo, puede ser copiada por sitios de phishing que suplantan a Discover. Evitamos que los enlaces maliciosos naveguen fuera de Discover impidiendo la navegación superior mediante <iframe sandbox>. El marco exterior sólo puede escaparse navegando directamente a otro sitio.

Cookies del lado del cliente

El document.cookie permite a JavaScript leer y modificar las cookies que no están marcadas HttpOnly. Soportar esto de forma segura es un reto en un sistema que mantiene las cookies en el servidor.

Acceso a las cookies: Cuando se recibe una petición, el proxy enumerará todas las cookies que son visibles para ese origen. Luego adjuntará una carga útil JSON a la página de respuesta. El código del lado del cliente se inyecta para calzar document.cookie y hacer que estas cookies sean visibles para otros scripts, como si fueran cookies reales del lado del cliente.

Modificación de cookies: Si se permite a los scripts establecer arbitrariamente cookies que el servidor luego acepta, esto podría llevar a la fijación, donde el origen evil.com podría establecer una cookie sensible en example.com.

Confiar en las capacidades CORS del navegador no sería suficiente en este caso – el origen a.example.com tratando de establecer una cookie en example.com será bloqueado por el navegador, ya que estos orígenes son hermanos y no jerárquicos.

Aún así, cuando el servidor recibe una nueva cookie establecida por el cliente, no puede hacer valer de forma segura si el dominio de destino está permitido; el origen del escritor sólo se conoce en el cliente y no siempre se envía al servidor de una forma en la que podamos confiar.

Para forzar al cliente a demostrar que es elegible para establecer cookies en un dominio específico, el servidor enviará, además de la carga útil JSON, una lista de tokens criptográficos para cada uno de los orígenes en los que el origen solicitante está autorizado a establecer cookies. Estos tokens están salados con el valor ick, por lo que no pueden ser transferidos entre usuarios.

El shim del lado del cliente para document.cookie se encarga de resolver e incrustar el token en el texto real de la cookie que se envía al proxy. El proxy puede entonces verificar que el origen de escritura poseía efectivamente el token para escribir en el dominio de destino de la cookie, y lo almacena en el tarro de cookies del lado del servidor, enviándolo de nuevo al cliente la próxima vez que se solicite la página.

Protocolo Bootstrap

El modelo contiene tres tipos de origen: origen de portal (portal Discover, etc.), origen seguro (marco exterior), y origen de reescritura (marco interior). Cada uno tiene una necesidad diferente:

  1. El origen portal requiere datr.
  2. El origen seguro requiere ickt.
  3. El origen de reescritura requiere datr y ick.

Con el protocolo localStorage

Aquí hay una representación del proceso de bootstrap para la mayoría de los navegadores móviles modernos:

Es importante notar que para evitar la reflexión, el punto final de bootstrap en el origen seguro siempre emite un nuevo ick y ickt; ick nunca depende de la entrada del usuario. Tenga en cuenta que, como establecemos domain=.discoverapp.com en ick y datr, están disponibles en todos los tipos de origen, y ickt sólo está disponible en el origen seguro.

Sin protocolo localStorage

Debido a que ciertos navegadores, como Opera Mini (popular en muchos países donde opera Discover), no soportan localStorage, no podemos almacenar los valores de ick y ickt. Esto significa que tenemos que utilizar un protocolo diferente:

Decidimos separar el origen de reescritura del origen seguro para que no compartan el mismo sufijo de host según la lista de sufijos públicos. Utilizamos www.0.discoverapp.com para almacenar la copia segura de ickt (como una cookie), y movemos todos los orígenes de terceros bajo 0.i.org. En un navegador de buen comportamiento, establecer una cookie en el origen seguro lo hará inaccesible a todos los orígenes de reescritura.

Como los orígenes están ahora separados, nuestro proceso de arranque se convierte en un proceso de dos pasos. Antes, podíamos establecer ick en la misma petición que aprovisionamos localStorage con ickt. Ahora, necesitamos arrancar dos orígenes, en peticiones separadas, sin abrir vectores de fijación ick.

Resolvemos esto arrancando primero el origen seguro con la cookie ickt y dando al usuario una versión encriptada de ick, con una clave conocida sólo por el proxy. El texto cifrado ick va acompañado de un nonce que puede utilizarse para descifrar ese ick en particular en el origen de reescritura y establecer una cookie, pero sólo una vez.

Un atacante podría elegir entre:

  1. Utilizar el nonce para revelar la cookie ick.
  2. Pasarlo al usuario para fijar su valor.

En cualquiera de los dos casos, el atacante no puede conocer y forzar simultáneamente un valor ick concreto en un usuario. El proceso también sincroniza datr entre los orígenes.

Esta arquitectura ha sido sometida a importantes pruebas de seguridad internas y externas. Creemos que hemos desarrollado un diseño que es lo suficientemente robusto como para resistir los tipos de ataques a aplicaciones web que vemos en la naturaleza y entregar de forma segura la conectividad que es sostenible para los operadores móviles. Tras el lanzamiento de Discover en Perú, tenemos previsto realizar más pruebas de Discover con operadores asociados en otros países en los que hemos estado probando las características del producto en fase beta, como Tailandia, Filipinas e Irak. Anticipamos que Discover estará en vivo en estos países adicionales en las próximas semanas, y exploraremos pruebas adicionales donde los operadores asociados quieran participar.

Nos gustaría agradecer a Berk Demir por su ayuda en este trabajo.

En un esfuerzo por ser más inclusivo en nuestro lenguaje, hemos editado este post para reemplazar «whitelist» por «allowlist».

Deja una respuesta

Tu dirección de correo electrónico no será publicada.