Reports
MEDIA[LÓGICA DE NEGOCIO]#3686283

Monero GUI: la firma offline en carteras de solo lectura se salta el bloqueo de IDs de pago largos

En monero-gui, el botón «Create» de la firma offline de transacciones no comprobaba el aviso de ID de pago largo que sí bloquea «Send», lo que permitía preparar transacciones con metadatos sin cifrar que pueden vincular pagos.

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

Los IDs de pago largos (64 caracteres hexadecimales, independientes de la dirección) son un mecanismo obsoleto de Monero: viajan sin cifrar en la cadena de bloques y permiten relacionar pagos. La interfaz gráfica oficial, monero-gui, lo sabe y actúa en consecuencia: cuando la página de transferencia recibe uno, muestra un aviso y desactiva el botón Send.

El fallo está en que esa misma protección no se aplicaba a otro camino para crear transacciones: la firma offline que usan las carteras de solo lectura (view-only). En pages/Transfer.qml, el botón Create de «Offline transaction signing» seguía activo aunque el aviso estuviese visible, y enviaba el ID de pago al mismo manejador que usa Send y, desde ahí, al backend en C++ que construye la transacción.

Es un fallo de mecanismo de protección (CWE-693), con una posible clasificación secundaria como exposición de información (CWE-200), porque la consecuencia es una fuga de privacidad a través de los metadatos de la transacción. El reporte figura con severidad media; el investigador propuso este vector CVSS 3.0:

CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N

Cómo entra el ID de pago

El dato llega desde fuera: una URI monero: o un código QR. Tanto main.qml (función onUriHandler) como components/QRCodeScanner.qml analizan la carga con walletManager.parse_uri_to_object() y pasan el payment_id a la vista de transferencia, que lo guarda en el campo de la interfaz:

// pages/Transfer.qml:119-122
function setPaymentId(value) {
    paymentIdLine.text = value;
    paymentIdCheckbox.checked = paymentIdLine.text != "";
}

Al marcarse esa casilla aparece el aviso paymentIdWarningBox («Long payment IDs are obsolete… would harm your privacy»).

La diferencia entre los dos botones

El botón Send incluye el aviso en su condición de activación:

// pages/Transfer.qml:831-843
enabled: !sendButtonWarningBox.visible && !warningContent && !recipientModel.hasEmptyAddress() && !paymentIdWarningBox.visible

El botón Create de la firma offline, en cambio, solo comprueba que la cartera sea de solo lectura, que la información esté completa y que el nodo esté sincronizado:

// pages/Transfer.qml:933-944
button1.enabled: appWindow.viewOnly && pageRoot.checkInformation() && appWindow.daemonSynced
button1.onClicked: {
    ...
    setPaymentId(paymentIdLine.text.trim());
    root.paymentClicked(recipientModel.getRecipients(), paymentIdLine.text, root.mixin, priority, descriptionLine.text)
}

Ambos acaban en handlePayment() de main.qml (líneas 938-970), que llama a currentWallet.createTransactionAsync(addresses, paymentId, amountsxmr, mixinCount, priority) o a createTransactionAllAsync(...). El envoltorio C++ de src/libwalletqt/Wallet.cpp conserva el valor y lo pasa a m_walletImpl->createTransactionMultDest(destinations, payment_id.toStdString(), ...).

Para completar el cuadro, el diálogo de confirmación (components/TxConfirmationDialog.qml) enseña los destinatarios y sus direcciones, pero no el ID de pago, así que el usuario no tiene una segunda oportunidad de verlo antes de crear la transacción.

Pasos de reproducción

El investigador plantea estos pasos a partir de la revisión del código fuente (los presenta como reproducción teórica, no como una prueba ejecutada), para un entorno local con una cartera de prueba, sin fondos reales ni nodos públicos:

  1. Abrir monero-gui con una cartera de prueba de solo lectura.
  2. Activar las opciones avanzadas de transferencia.
  3. Esperar a que la cartera llegue al estado en el que está disponible la creación de transacciones offline.
  4. Cargar una URI monero: o un código QR que contenga una dirección de destino válida de prueba, una cantidad de prueba y un ID de pago independiente de 64 caracteres hexadecimales.
  5. Comprobar que la página de transferencia guarda el ID en paymentIdLine.text.
  6. Comprobar que aparece el aviso de ID de pago largo.
  7. Comprobar que el botón Send está desactivado, porque su condición incluye !paymentIdWarningBox.visible.
  8. Comprobar que el botón Offline transaction signing → Create sigue activo, porque su condición es solo:
appWindow.viewOnly && pageRoot.checkInformation() && appWindow.daemonSynced
  1. Pulsar Create.
  2. El ID de pago recorre pages/Transfer.qml:942-943, main.qml:938-970, src/libwalletqt/Wallet.cpp:659-668 y src/libwalletqt/Wallet.cpp:631-655 hasta llegar a la creación de la transacción en el backend.

Resultado: el camino de Send bloquea la creación de la transacción, pero el de Create sigue activo y entrega el mismo ID de pago al backend, cuando lo esperado sería que aplicase el mismo bloqueo.

Impacto

Quien controle la solicitud de pago (un atacante, un comercio o un generador de solicitudes comprometido) puede meter un ID de pago largo en una URI o en un QR. Con el flujo normal la aplicación lo bloquea; con el flujo de firma offline, no:

  • El usuario prepara sin saberlo una transacción sin firmar que lleva metadatos sin cifrar y vinculables.
  • El diálogo de confirmación no muestra ese ID, así que no hay aviso claro antes de crearla.
  • Si la transacción se firma después y se difunde, el ID de pago queda en la cadena y puede servir para relacionar o identificar el pago.
  • Afecta precisamente a los flujos de cartera de solo lectura y firma en frío, que usan quienes más cuidan su seguridad y que pueden dar por hecho que la aplicación aplicó las mismas restricciones de privacidad que al enviar normalmente.

No permite saltarse la firma ni robar fondos: el daño se limita a la privacidad de la transacción, de ahí la severidad media.

Remediación

El reporte no detalla el parche aplicado, solo que el problema figura como resuelto. La corrección esperada, según el investigador, es que la creación offline aplique la misma protección que Send: si la aplicación bloquea el envío normal por un ID de pago largo, también debe bloquear la creación de la transacción offline con ese mismo ID.