Reports
MEDIA[LÓGICA DE NEGOCIO]#3648638

Monero GUI: un deeplink monero:// con tx_amount=(all) activa el modo «enviar todo el saldo»

La interfaz gráfica de Monero procesaba los enlaces monero:// externos sin validar el importe, de modo que el valor interno "(all)" colaba una transferencia de todo el saldo desbloqueado hacia la dirección del atacante.

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

Monero GUI, la cartera de escritorio de Monero, tiene dos caminos distintos para interpretar una URI de pago. Cuando la URI llega por código QR o se pega a mano, pasa por el parser del backend en C++, que trata el importe como un número (uint64_t). En cambio, los deeplinks externos monero:// se procesan con una rutina propia escrita en QML dentro de main.qml, y esa rutina entrega el parámetro tx_amount tal cual, como texto, al modelo de la transferencia.

El problema está en que ese modelo usa la cadena literal "(all)" como valor de control interno para decir «enviar todo el saldo desbloqueado». Como el camino de los deeplinks no filtra ese valor, cualquiera puede construir un enlace que lo incluya y la cartera preparará una transferencia de todo el saldo a la dirección que indique el enlace:

monero://<attacker_address>?tx_amount=(all)&tx_description=test

Se trata de un error de lógica de negocio (así lo clasifica el reporte, sin número CWE) observado en el código fuente correspondiente a la versión 0.18.4.6. El reporte tiene severidad media y consta como resuelto; se envió el 4 de abril de 2026 y se hizo público el 20 de agosto de 2026.

Pasos de reproducción

El investigador lo demostró revisando el código y validando localmente que se llega a la rama vulnerable, sin carteras ni fondos reales. El recorrido completo es este:

  1. En main.qml:447-478, la función onUriHandler(uri) descompone a mano los parámetros de la URI monero:// y pasa el valor sin procesar de params["tx_amount"] a middlePanel.transferView.sendTo(...).
  1. En pages/Transfer.qml:95-105, fillPaymentDetails(...) mete ese importe en el modelo de destinatarios sin comprobar que sea un número:

`` recipientModel.newRecipient(address, Utils.removeTrailingZeros(amount || "")) ``

  1. En pages/Transfer.qml:203-212, recipientModel.getAmountTotal() reconoce el literal exacto "(all)" como valor especial y devuelve el saldo desbloqueado de la cartera.
  1. En main.qml:964-980, handlePayment() comprueba si el importe de algún destinatario es "(all)". Si lo es, no sigue el camino normal de creación de transacciones con importe numérico, sino que llama a:

`` currentWallet.createTransactionAllAsync(...) ``

  1. Como comparación, el parser canónico de src/libwalletqt/WalletManager.cpp:406-423 guarda el importe en un uint64_t amount y luego lo vuelve a convertir a texto para mostrarlo, así que por esa vía no puede llegar el valor "(all)".
  1. Con todo lo anterior, basta abrir un deeplink como este para llegar a la condición que activa el envío de todo el saldo:

`` monero://<attacker_address>?tx_amount=(all)&tx_description=test ``

El resultado es que la cartera prepara una transferencia de todo el saldo desbloqueado hacia la dirección del atacante, cuando lo esperable sería que una URI externa solo admitiera importes numéricos validados por el backend.

Impacto

Un atacante puede convertir lo que parece una solicitud de pago normal en una orden de enviar todo el saldo desbloqueado a su propia dirección. No es un robo sin interacción: la víctima tiene que abrir el enlace y confirmar la transacción en la interfaz. Pero el atacante decide si el enlace se interpreta como un importe concreto o como «todo el saldo», y eso hace mucho más eficaces las campañas de phishing y los engaños con solicitudes de pago. En una cartera de criptomonedas, es un fallo de integridad de las transacciones.

Remediación

El reporte no explica cómo se corrigió; solo consta como resuelto. Lo que propone el investigador es que las URIs de pago externas solo acepten importes numéricos procesados por el parser canónico del backend, y que los valores de control internos de la interfaz, como "(all)", no puedan llegar nunca desde una entrada externa no confiable.