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
- Arquitectura inicial
- Diseño del dominio
- Cookies
- Mejorando lo que habíamos construido
- Mejoras en la arquitectura de Discover
- JavaScript y fijación de cookies
- Solución de dos marcos
- Marco interior
- Marco exterior
- Interacción con la página
- Fijación asíncrona de cookies
- Clickjacking
- Phishing
- Cookies del lado del cliente
- Protocolo Bootstrap
- Con el protocolo localStorage
- Sin protocolo localStorage
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:
- 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 subdominiofree.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.
- 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 certificadowww.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:
- 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. - 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, sia.example.com
establece una cookie en.example.com
, entoncesb.example.com
debería poder leerla. En el caso de Free Basics,a-example-com.0.freebasics.com
establecería una cookie enexample.com.0.freebasics.com
, lo que no está permitido por la norma. Como eso no funciona, otros orígenes, comob-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:
- La cookie
datr
, un identificador del navegador utilizado con fines de integridad del sitio. - 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:
- Las cookies del lado del servidor se cifran con una
ick
que se guarda sólo en el cliente. - Cuando el cliente proporciona la
ick
, ésta es olvidada por el servidor en cada petición sin llegar a ser registrada. - Marcamos las dos cookies del lado del cliente como
Secure
yHttpOnly
. - 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.
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:
- Al registrarse, generamos una nueva y segura
ick
aleatoria. - Enviamos
ick
al navegador como una cookieHttpOnly
. - Entonces hacemos un HMAC de un valor llamado
ickt
a partir de un compendio tanto deick
como dedatr
(para evitar la fijación de ambos) y almacenamos una copia deickt
en el cliente, en una ubicación enlocalStorage
en la que un potencial atacante no pueda escribir. La ubicación que utilizamos eshttps://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. - Incorporamos
ickt
, derivado de la cookieick
vista en la petición, dentro del HTML en cada respuesta de proxy de terceros. - Cuando se carga la página, comparamos la
ickt
incrustada con laickt
de confianza utilizandowindow.postMessage()
, e invalidamos la sesión si hay una discrepancia eliminando las cookiesdatr
yick
. - 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:
- Comprueba con el marco externo:
-
postMessage
a la parte superior conickt
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.
-
- Comprueba con
parent
:-
postMessage
aparent
. - 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:
- 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.
- 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:
- Nos aseguramos de que el marco exterior sea siempre el marco superior con JavaScript y
X-Frame-Options: DENY
. - Espera a
postMessage
. - 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.
- 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:
- Fijación de cookie asíncrona.
- Clickjacking debido a framing.
- Phishing suplantando el dominio Discover.
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:
- El origen portal requiere
datr
. - El origen seguro requiere
ickt
. - El origen de reescritura requiere
datr
yick
.
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:
- Utilizar el nonce para revelar la cookie
ick
. - 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».