
La semana pasada se publicó uno de esos writeups que valen oro para cualquiera que trabaje con servidores: un investigador encontró ejecución remota de código en la plataforma de documentación de Google, developers.google.com, y cobró USD 90.000 por ello vía Bug Hunters. Y la parte que me interesa no es la cifra, sino lo simple que era el problema: sesiones de Flask firmadas con una llave pública y pickle.loads en el camino de vuelta.
La cadena completa
El flujo era este: al entrar al OAuth público de la página de docs, te entregaban un cookie de sesión Flask de unos 1.200 bytes — de más, considerando que solo debía llevar un token CSRF y una URL de retorno. Flask firma sus sesiones pero no las cifra, así que cualquiera con el cookie puede decodificarlo. Ahí dentro había un objeto pickle serializado, que a su vez traía en texto plano un client_id y un client_secret de OAuth de Google.
Y acá viene lo bueno. La firma del cookie validaba contra ese mismo client secret. O sea: la llave que firma la sesión estaba guardada dentro del objeto que la sesión misma firmaba. Un secreto con dos trabajos, y el investigador lo comprobó de forma forense: derivó la llave con HMAC tal como lo hace Flask, recalculó la firma fuera de línea y el servidor la aceptó. Session integrity — la única barrera — fuera del juego.
Con eso, el pickle del callback pasó de «inocuo» a letal. Un pickle no es datos: al deserializar, puede ejecutar lo que sea. Como el cookie se podía falsificar con la firma correcta, era ejecución remota de código sin autenticación. La prueba fue elegante — payloads con time.sleep(N) variando N, midiendo la latencia de la respuesta del server. Sin correr un solo comando en producción.
Por qué no era un único sitio
La llave era de la plataforma DevSite compartida, así que el mismo cookie firmaba sesiones válidas en firebase.google.com, developer.android.com, source.android.com y otros hosts de docs de Google. La superficie crece solo. Y el arreglo obligó a todo lo que en mi pega suelo repasar como checklist: rotar los secretos expuestos y matar todas las sesiones firmadas con ellos; dar a cada despliegue su propia llave de alta entropía que no tenga ninguna relación con otra credencial; dejar el estado del OAuth en el servidor con un cookie opaco; y abandonar pickle para todo lo que cruce límite de cliente. Con cualquiera de los cuatro el ataque caía, pero necesitaron los cuatro juntos para llegar hasta acá.
Lo que no me sorprende pero igual me molesta
La ventana entre reporte (6 de agosto) y fix (7 de octubre) fue de dos meses. Para un RCE sin autenticación que comparte llave entre varias propiedades de Google. Entiendo que una plataforma con ese tráfico no se parcha de un día para otro — rotar una llave de sesiones mata logins en masa — pero dos meses es el plazo que le dan a un proveedor para que se organice, no el que la superficie merece.
Mi lectura: el patrón culpable es poner un objeto interno serializado donde el cliente pueda tocarlo, y protegerlo con un secreto que viaja junto al resto. Lo he visto en apps self-hosted de tamaño chico, no hace falta ser Google. Si tu Flask guarda en el cookie algo que no sea un ID opaco, ese algo es tu siguiente incidente esperando firma válida.
Fuente de inspiración: How I Found a $90,000 Remote Code Execution Vulnerability in Google
