Reports
CRÍTICA[AUTENTICACIÓN]#2976481

Cosmos SDK: robar fondos desbloqueados de cuentas lockup ajenas falsificando el remitente del mensaje

Las cuentas de bloqueo (lockup) de x/accounts en Cosmos SDK comprobaban el propietario con el campo Sender del propio mensaje, que nadie validaba. Cualquiera podía mover los fondos ya desbloqueados de una cuenta ajena.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

Resumen

El módulo x/accounts de Cosmos SDK incluye unas cuentas predefinidas de bloqueo de fondos (lockup), entre ellas periodic-locking-account: los tokens quedan inmovilizados durante un periodo y, cuando se liberan, el propietario puede enviarlos con la función SendCoins (x/accounts/defaults/lockup/lockup.go).

Antes de enviar, SendCoins llama a checkSender, que comprueba que el remitente sea el dueño de la cuenta. El problema está en de dónde sale ese remitente: se lee de msg.Sender, un campo que viene dentro del propio mensaje. Ese mensaje llega empaquetado en un MsgExecute, y en ningún punto se comprueba que msg.Sender coincida con quien firmó la transacción. Es decir, el atacante escribe en el mensaje la dirección del propietario y la comprobación pasa.

Lo correcto ya existía en el propio SDK: al ejecutar un MsgExecute, el keeper de x/accounts (x/accounts/keeper.go) guarda el remitente real en el contexto, y la cuenta multifirma (x/accounts/defaults/multisig/account.go) lo toma de ahí. Las cuentas lockup, en cambio, se fiaban del mensaje.

El investigador hizo la prueba de concepto con periodic-locking-account, pero señala que el resto de cuentas lockup probablemente también estaban afectadas, porque todas usan la misma SendCoins. HackerOne la clasifica como control de acceso indebido (genérico), severidad crítica. Hubo recompensa, de importe no público. Se envió el 6 de febrero de 2025 y se divulgó el 29 de junio de 2025.

Pasos de reproducción

Hacen falta Go y Rust. El investigador adjuntó un script para levantar la cadena (setup_chain) y un proyecto Rust con el ataque (main.rs, types.rs, client.rs, func.rs, msg.rs y Cargo.toml), además de un vídeo de la prueba.

  1. Descargar la última versión de cosmos-sdk y compilarla:
make build
  1. Anotar la ruta del binario generado y ponerla en el script setup_chain.
  2. Crear un proyecto Rust con el Cargo.toml adjunto y copiar todos los ficheros .rs en la carpeta src.
  3. Dar permiso de ejecución a setup_chain, lanzarlo y esperar a que la cadena arranque:
./setup_chain
  1. Ejecutar el proyecto Rust. El escenario de la prueba tiene dos fases:
  • Se crea una periodic-locking-account para la víctima, con un periodo de bloqueo corto para no tener que esperar mucho.
  • Cuando termina el periodo de bloqueo, el atacante transfiere los fondos de esa cuenta a la suya, enviando la orden dentro de un MsgExecute con la dirección de la víctima como Sender.

Impacto

Un atacante puede quedarse con los fondos de una cuenta lockup que no le pertenece una vez desbloqueados (por ejemplo, al terminar el periodo de bloqueo). La prueba se hizo con periodic-locking-account, pero hay motivos para pensar que afecta a más tipos de cuenta lockup. Según el investigador, no había ninguna forma de mitigarlo mientras no se corrigiera el código.

Qué aprender de este caso

  • En Cosmos SDK y frameworks parecidos, cuando un mensaje va envuelto en otro (MsgExecute y similares), busca manejadores que lean la identidad de un campo Sender/From del mensaje interno: si ese campo nunca se compara con el firmante, es una suplantación directa.
  • Compara cómo resuelven el remitente distintos tipos de cuenta del mismo módulo: aquí la multifirma lo tomaba del contexto y las lockup del mensaje. Una inconsistencia así entre implementaciones hermanas es una buena pista para auditar.
  • Al programar, la identidad para autorizar debe salir del contexto de ejecución que rellena el framework a partir de la firma, nunca de datos que el usuario puede escribir. Si el mensaje lleva remitente, compruébalo contra el firmante o elimínalo.
  • Un fallo en una función compartida (aquí SendCoins) afecta a todas las variantes que la usan: al encontrarlo en una, revisa las demás.