Fotografía cortesía de Isis França 🧡
Portada del post

El hacking a Google

Entrada resubida, rescatada de tantas tonterías que escribí hace un par de años. Contra todo pronóstico resulta ser que interesó a algunas personas, las mismas que me pidieron la resubida para mantener ciertas fuentes. Aunque he borrado la parte personal dado que estoy preparando otra entrada relacionada a salud mental.

Mi último año por la universidad cursé una asignatura de desarrollo de aplicaciones web de la mano de un profesor al que estaré eternamente agradecido: Abraham Rodríguez, el ahora vicegerente de Agenda Digital de la ULPGC, lo que carajo sea que eso signifique…

Una de las primeras tareas consistía en hacer una aplicación con Angular 2+ e Ionic que de backend utilizara el servicio Firebase de Google.

Por aquel entonces nos hacían trabajar con la base de datos en tiempo real de dicho servicio, algo que podía explotarse muy bien con la librería RxJS —la implementación de ReactiveX en JavaScript— para obtener reactividad de datos a nivel de servicio y a través de sockets. Se hacía uso del framework NgRx sobre ella, que facilitaba las tareas de comunicación y control de estado centralizado, distribuido en red y reactivo en Angular.

Mi descubrimiento de ReactiveX y los diagramas reactivos me emocionó, muchísimo, era un nuevo mundo que bebía del mecanismo, patrón, técnica, paradigma y casi filosofía de desarrollo que más me gustaba: era la evolución del patrón observer y la programación reactiva en general, llevada a red. Aún ni conocía qué era la Event-Driven Architecture, pero sí me sentía atraído por una comunicación vía observación, eventos y mensajes. Sabía que sería un trabajo que disfrutar como un niño pequeño. El problema es que Google decidió poner obstáculos.

Las antiguas limitaciones de Cloud Functions

Concretamente solo me tocó hacer un mero chat por salas, pero Google me bloqueaba un servicio de Firebase fundamental: Cloud Functions. Lo cierto es que su único requisito era que pusiera una tarjeta de crédito o débito y ni siquiera conllevaba ningún cargo inicial salvo que gastaras mucho tiempo de ejecución o hicieras miles de llamadas a las funciones.

Cloud Functions es una característica y framework de Firebase que permite ejecutar código JS en la nube a través de peticiones http y que se integraba correctamente con la base de datos en tiempo real (RTDB, del inglés) y otras features cloud del servicio. Todas estas historias que se popularizaron como serverless computing y permitían hacer una API rápidamente.

Muy convenientemente para Google estas funciones podían entrar en una suerte de recursividad casi infinita si tenías un error, generando facturas de cientos de euros en una noche si no te dabas cuenta. Por ello nunca deberías usar tu tarjeta en un servicio que no te establezca un límite duro de cuotas y te de opción de detener el servicio si se superan en lugar de cobrarte por exceso. Y no, no vale una tarjeta monedero, acabarás con una deuda que Google intentará resolver de forma muy activa por lo que me han contado.

Ignorando a Cloud Functions, los primeros cuatro días los invertí buscando formas de conseguir un CRUD perfecto con validación en backend usando exclusivamente la base de datos y su sistema de permisos integrados con su sistema de auth. No tardaron en aparecer los primeros inconvenientes.

Necesitaba algunas funciones relacionadas a la creación de salas y el límite de salas que podía tener un único usuario, algo que requería algo más de lógica. Desde una API con Cloud Functions no costaba nada, mientras que por razones de responsabilidad no eran posibles de realizar en la base de datos. Los esquemas de la RTDB eran más reducidos que ahora.

Modelo describiendo la comunicación tradicional con Firebase
Modelo ideal. No válido dado que Google bloqueaba cualquier tipo de petición externa a Cloud Functions si no tenías tarjeta de crédito o débito.

Pero Cloud Functions en realidad sí estaba disponible, lo único que limitaba eran las llamadas a la API de manera externa, vamos, justamente lo que más necesita alguien que quiera usar el servicio.

Lo que exploté es que sí permitía definir “triggers” o disparadores. Funciones que se ejecutaran ante situaciones concretas en la base de datos, como que un usuario escribe un dato en determinado nodo.

El servicio no garantizaba que la ejecución de esos disparadores fuera inmediata. Además, solo se podía comunicar con la base de datos, no podía responder hacia fuera de ninguna manera… ¿o sí? 🙂

Los observables

La gracia de la RTDB es que es en tiempo real como su nombre indica. Esto significa que puedes observar cualquier nodo mediante un socket y que te sea notificado cuando cambie. Esto es lo que permite, por ejemplo, que veas mensajes aparecer en un chat sin tu pulsar un botón de refrescar.

Ahora supongamos una base de datos en la que se reserva para cada usuario un nodo con 2 subnodos a los que coherentemente llamamos request y response:

...,
userRequests: {
    v8934sdafbi4r3vfewsjkvwevqwe: {
        request: { ... },
        response: { ... }
    },
    wigr908u43nwpdfglxjretjkhne3: {
        request: { ... },
        response: { ... }
    }
}

Cuando un usuario quiere hacer una petición POST, en lugar de hacer una petición HTTP a Cloud Functions con el clásico fetch de JavaScript, puede escribir directamente en su nodo request adecuado en la RTDB con NgRx. Las ristras largas y aleatorias representan la ID de un usuario autenticado, que bien podría ser un UUID estándar. En su propio nodo request —que es donde único tendría permiso de escritura un usuario— puede establecer los datos de la petición.

El exploit reside en la configuración de un trigger que se disparará cuando un usuario escriba su request. Este nodo solo tiene parámetros como si de una función post se tratara. El trigger ya me adelanta parte del trabajo porque por defecto recibe información acerca del usuario que ha causado el cambio y el nodo cambiado. Por tanto, escribir en el nodo request provoca la llamada a una función que conocerá dicho contexto siendo, en efecto, una forma de invocar una función con argumentos.

...,
userRequests: {
    v8934sdafbi4r3vfewsjkvwevqwe: {
        request: {
            id: '4afb4ef5065a',
            op: 'CREATE_ROOM',
            body: { password: '1234' }
        },
        response: {
            id: '4afb4ef5065a',
            body: { createdRoomID: 'x87bhu43'}
        }
    },
    ...
}

Si se quiere crear una sala de chat, el usuario solo tendría que escribir en su nodo request un UUID aleatorio generado previamente a fin de tener algo que identifique la petición, la operación que quiere realizar (CREATE_ROOM en este caso) y los parámetros que pueda requerir dicha operación. En este ejemplo el único parámetro se trataba de una contraseña para la sala.

En resumen: hice una API a través de una base de datos. La forma de recibir respuesta es mediante eventos, usando NgRx se observa el nodo response. Sabrá que ha sido respondida cuando la ID en response coincida con la ID enviada. Y la respuesta la recibirá en el nodo body de response. En este caso el servidor da una ID de la sala, la típica que compartir con tus amigos para que entren como se hace en juegos online o Hangouts (EDIT: actualmente Meets)

diagrama describiendo la comunicación con el servicio faked
Modificaciones. Las peticiones por fetch son reemplazadas por escrituras en la RTDB que disparaban triggers de Cloud Functions. La instancia concreta de FakeAPIService en realidad se inyecta. Nota en 2024: Ahora soy consciente de que, aunque correcto, el diagrama es bastante feucho. En esos tiempos no conocía formas más claras de representar inyecciones de dependencias, inversiones de control y otras tantas cosas.

Todo este proceso es transparente a la aplicación. Podemos abstraer una interfaz del servicio original y mantener las dos implementaciones si queremos, como se muestra en el diagrama. Para la aplicación, la interfaz del servicio sería la misma, que puede trabajar con simples promesas como si de un fetch se tratara.

Claro que esto no es un uso realista de la base de datos. Estamos sobrecargándola y además implica tratar con unas esperas del trigger que a veces podía llegar a los 4 segundos. Para una operación de creación de un recurso no es algo tan bestial, pero estas esperas reducen drásticamente la aplicabilidad en muchos otros casos de uso. También, de esta manera es más fácil gastar la cuota de uso de la base de datos, aunque su ampliación sea más barata que Cloud Functions en sí misma.

Las consecuencias

Las consecuencias no fueron más que gotas a un vaso a punto de rebosar o leña para una hoguera que comenzaba a arder. En la versión original de esta entrada hablaba de una crisis que mezcla obsesión, irresponsabilidad afectiva y retraso madurativo a partes iguales. En los próximos días publicaré algo al respecto (UPDATE del 28 noviembre de 2024: sigue pendiente). De momento prefiero no mezclar cosas.

Nunca he tenido la intención de usar la acepción de hacking como cibercrimen o nada relacionado a seguridad, sino como sobrepasar los límites de una tecnología o técnica para una aplicación mayor o con un uso no pensado. Sin embargo, esto sí era un claro bypass a una restricción de servicio. De publicarse y popularizarse estoy seguro de que como mínimo Google habría limitado mucho más a Cloud Functions si ésta era una de sus principales características monetizadas.