Resumen
El Starknet Snap de Consensys es una extensión para MetaMask que permite usar cuentas de la red Starknet. Cuando una web le pide firmar algo, el Snap debería mostrar siempre un diálogo para que el usuario revise el contenido y lo acepte o lo rechace. El problema estaba en que la decisión de mostrar o no ese diálogo dependía de un parámetro, enableAuthorize, que envía la propia web que hace la petición.
Es decir, la parte que debía ser vigilada decidía si se la vigilaba. Una web maliciosa solo tenía que mandar enableAuthorize a false para que el Snap firmara directamente con la clave privada de la cuenta, sin preguntar nada.
El fallo está en el manejador de la RPC de firma de mensajes, packages/starknet-snap/src/rpcs/sign-message.ts (línea 77 en la rama main, según el investigador):
protected async handleRequest(
params: SignMessageParams,
): Promise<SignMessageResponse> {
const { enableAuthorize, typedDataMessage, address } = params;
if (
// Get Starknet expected not to show the confirm dialog, therefore, `enableAuthorize` will set to false to bypass the confirmation
// TODO: enableAuthorize should set default to true
enableAuthorize &&
!(await renderSignMessageUI({
address,
typedDataMessage,
chainId: this.network.chainId,
}))
) {
throw new UserRejectedOpError() as unknown as Error;
}
return await signMessageUtil(
this.account.privateKey,
typedDataMessage,
address,
);
}
}
La condición empieza por enableAuthorize &&: si el valor es falso, la expresión se corta ahí, renderSignMessageUI nunca se ejecuta y el código pasa directamente a signMessageUtil con this.account.privateKey. El propio comentario del código explica que el parámetro existía para que Get Starknet pudiera evitar el diálogo, y deja un TODO indicando que debería valer true por defecto.
HackerOne lo clasifica como error de lógica de negocio (Business Logic Errors), con severidad media. El reporte se resolvió y recibió recompensa, cuyo importe no es público.
Pasos de reproducción
- Tener MetaMask instalado en el navegador.
- Entrar en
https://snaps.consensys.io/starknety pulsar "Connect with MetaMask" para instalar el Starknet Snap. - Descargar la página de prueba que adjuntó el investigador,
exp-starknet.html, y servirla en local en el puerto 5555. - Abrir
http://localhost:5555/exp-starknet.html. - Pulsar el botón "start PoC".
- Aceptar la solicitud de conexión.
- Comprobar que la página muestra el resultado de
signMessagesin que el usuario lo haya aprobado.
El contenido de exp-starknet.html no se ha hecho público.
Impacto
Basta con que el usuario visite una web maliciosa y acepte conectarla, algo habitual en cualquier aplicación descentralizada. A partir de ahí, la web puede obtener firmas de mensajes arbitrarios con la clave de su cuenta de Starknet sin que el usuario vea qué se firma. Según el investigador, lo mismo permite firmar transacciones sin aprobación, lo que puede acabar en el robo de los fondos de la cuenta.
Qué aprender de este caso
- En wallets, extensiones y Snaps, revisa los parámetros de las RPC de firma o envío: cualquier opción del estilo
enableAuthorize,skipConfirmosilentque llegue de la web es una posible forma de saltarse la confirmación del usuario. - La decisión de pedir confirmación tiene que tomarse dentro del componente que guarda la clave, nunca a partir de lo que mande quien pide la firma. Si un cliente concreto necesita evitar el diálogo, debe identificarse por su origen verificado, no por un parámetro.
- Busca condiciones como
flag && !(await confirmar(...)): siflagllega vacío ofalse, la confirmación desaparece en silencio. Lo seguro es que la ausencia del valor equivalga a pedir confirmación. - Los comentarios
TODOjunto a controles de seguridad, como el de este código, son una buena pista de dónde mirar al auditar un proyecto de código abierto.