Resumen
monero-wallet-rpc puede arrancarse con --restricted-rpc, un modo pensado para dar acceso limitado a la cartera (por ejemplo a un agente de monitorización, una integración de pagos o un pipeline de CI) sin permitir operaciones que muevan fondos. En el commit 604bb05d8 (PR #10378, fusionado el 23 de marzo de 2026) se añadió una comprobación m_restricted a todos los métodos de tipo transferencia: transfer, sweep_all, transfer_split, sign_transfer, submit_transfer y sweep_dust. Uno se quedó fuera: relay_tx.
El fallo está en el manejador on_relay_tx de src/wallet/wallet_rpc_server.cpp (rama master, commit 230de3794, 20 de abril de 2026), y son dos problemas que se suman:
- No comprueba el modo restringido. Cualquiera con credenciales restringidas puede invocarlo.
- Confía ciegamente en el
pending_txrecibido. El método toma un blob hexadecimal del llamante, lo deserializa comowallet2::pending_txy lo pasa tal cual awallet2::commit_tx, sin comprobar que esa transacción la haya generado la propia cartera.
El manejador vulnerable carece del guardia que sí tienen sus hermanos. Donde los demás empiezan rechazando la llamada en modo restringido, on_relay_tx pasa directo del if (!m_wallet) a deserializar el blob y llamar a commit_tx con él.
El segundo problema está en commit_tx (wallet2.cpp). Tras aceptar el daemon la transacción, la función modifica el estado de la cartera usando los índices de selected_transfers que vienen en el blob, y lo único que comprueba es que estén dentro del rango de la lista de transferencias (idx >= m_transfers.size()), no que correspondan a las entradas reales de la transacción. Con esos índices:
- escribe la transacción en
m_unconfirmed_txs, - guarda el
tx_keydel blob enm_tx_keys[txid], - marca esas salidas como gastadas con
set_spent(), - y borra de forma irreversible los nonces multifirma (
m_multisig_k) de esos índices.
En ningún punto se verifica que m_transfers[idx].m_key_image aparezca entre las entradas de la transacción (ptx.tx.vin). Así, el llamante puede enviar una transacción perfectamente válida que gasta sus propios fondos, pero indicar en selected_transfers índices de salidas de la cartera víctima. El daemon la acepta porque es criptográficamente correcta; la incoherencia solo afecta a la memoria de la cartera, que es justo donde hace daño.
Pasos de reproducción
El investigador aportó un script (poc_relay_tx_state_corruption.sh) que monta un entorno regtest local y completa el caso de principio a fin. A grandes rasgos:
- Arranca un
monerod --regtesten loopback, sin red. - Crea dos carteras:
wallet_a(atacante, sin restricciones) ywallet_b(víctima, con--restricted-rpc). - Mina bloques hacia ambas carteras para que tengan salidas disponibles.
- En
wallet_allama atransfercondo_not_relay=trueyget_tx_key=truepara obtener unpending_txfirmado pero sin difundir, del que el atacante controla todos los campos. - Envía ese blob al
relay_txdewallet_b. La llamadatransferenwallet_bse rechaza por estar en modo restringido (código-7, "Command unavailable in restricted mode."), perorelay_txno: se ejecuta ycommit_txcorre contra la lista de transferencias de la víctima. - Consulta
incoming_transfersyget_tx_keyenwallet_bpara confirmar la corrupción.
La salida del script confirma los tres efectos: relay_tx tiene éxito en la cartera restringida, dos salidas de la víctima pasan de "unspent" a "SPENT (frozen)", y el tx_key almacenado para esa transacción en la cartera víctima coincide con el del atacante.
Impacto
Un operador que ejecuta monero-wallet-rpc con --restricted-rpc —el modo que se supone seguro para dar acceso acotado— no tenía ninguna protección frente a este método. Cualquier proceso con credenciales restringidas podía llamar a relay_tx y disparar commit_tx sobre un blob manipulado. Si el atacante puede difundir una transacción válida (con sus propios fondos), consigue tres efectos concretos contra la cartera víctima:
- Congelación de fondos.
set_spent()marca como gastadas las salidas cuyos índices indique el blob. Quedan congeladas en la caché de la cartera y no se usarán en futuras transacciones hasta ejecutarrescan_blockchain. En una cartera caliente que procesa pagos, esto hace que los envíos fallen. - Envenenamiento del
tx_key.commit_txguarda eltx_keydel atacante enm_tx_keys[txid]. Cualquierget_tx_keyoget_tx_proofpara ese txid devuelve material de clave controlado por el atacante. Un comercio que useget_tx_proofpara verificar un pago recibiría una prueba que valida correctamente contra el txid pero generada con una clave que no es la suya, suficiente para falsificar confirmaciones de pago en cualquier sistema que confíe en las pruebas de wallet-rpc. - Rotura de multifirma.
commit_txborra incondicionalmentem_multisig_kde los índices seleccionados. Esos nonces de firma por transferencia no se pueden regenerar: una ronda de firma multifirma activa que dependiera de ellos queda rota de forma permanente y los cofirmantes tendrían que reexportar y reimportar claves.
El programa clasificó la severidad como baja ("low"); la debilidad se registró como "Improper Access Control - Generic".
Remediación
La corrección directa es añadir a on_relay_tx el mismo guardia que ya tenían los demás métodos, justo tras la comprobación !m_wallet:
if (m_restricted)
{
er.code = WALLET_RPC_ERROR_CODE_DENIED;
er.message = "Command unavailable in restricted mode.";
return false;
}
Eso cierra el salto del modo restringido. Como defensa en profundidad para el problema de fondo (que commit_tx confía en selected_transfers sin contrastarlo con las entradas reales de la transacción), el investigador propone verificar en el propio relay_tx, antes de llamar a commit_tx, que el número de selected_transfers coincide con el de entradas y que para cada índice la imagen de clave de la transferencia (m_key_image) coincide con la de la entrada correspondiente (k_image), rechazando la petición si no cuadra. Arreglar commit_tx directamente es más delicado porque también lo llaman rutas legítimas en las que la propia cartera construyó la transacción.
Qué aprender de este caso
- Cuando se endurece un conjunto de endpoints "sensibles" (aquí con el guardia
m_restricted), hay que auditar todos los que mutan el mismo estado, no solo los más evidentes:relay_txquedó fuera del parche aunque acaba llamando a la misma función quetransfer. Una forma de encontrar huecos así es listar todos los llamantes de la función peligrosa (commit_tx) y comprobar que cada uno tiene el control de acceso. - Deserializar una estructura completa que viene del cliente y pasarla a una función que modifica estado interno es un patrón de riesgo: revisa qué campos de ese objeto se usan como índices o claves (
selected_transfers,tx_key) y si se validan contra la realidad del objeto. Una comprobación de rango no es una comprobación de pertenencia. - En software de carteras y pagos, un fallo de "solo" corromper el estado local (congelar salidas, plantar claves) puede escalar a fraude si otros sistemas confían en sus salidas: aquí
get_tx_proofconvierte el envenenamiento deltx_keyen pruebas de pago falsificables. Al diseñar verificaciones de pago, no confíes en que una prueba validada contra un txid la haya generado tu propia cartera.