Monero · Report #3819475
Monero GUI OpenAlias DNSSEC-invalid resolution still writes spoofable address into recipient field
How it was found
Summary:
Monero GUI resolves OpenAlias names through DNS and exposes the DNSSEC result to QML as true|address or false|address. When DNSSEC validation fails but the TXT record contains a syntactically valid Monero address, TxUtils.handleOpenAliasResolution() returns both a warning message and the resolved address. The Transfer and Address Book pages then apply response.address to the recipient field even though response.message says the address may be spoofed.
Impact summary: an attacker who can influence a victim's DNS/OpenAlias lookup can make the GUI's OpenAlias helper produce an attacker-controlled address despite failed DNSSEC validation. The QML transfer flow then writes that unauthenticated address into the recipient field. If the victim proceeds with the send flow, funds are sent to the attacker-controlled address.
Affected Code & Version
Repository/version reviewed:
monero-guicommit003e667576f38812e27cbb79125a0ba96036225donmaster- CLI/library source reviewed from
/home/peko/monero/monerocommit2c48374ecd2449c02bb400e5bcf20b7c6f11649b
Affected code:
monero-gui/src/libwalletqt/WalletManager.cpp:391-396WalletManager::resolveOpenAlias()serializes the resolver result asdnssec_valid ? "true" : "false"plus the resolved address.
monero/src/wallet/api/wallet_manager.cpp:338-344WalletManagerImpl::resolveOpenAlias()returns the first address fromtools::dns_utils::addresses_from_url()even whendnssec_validis false.
monero/src/common/dns_utils.cpp:418-442addresses_from_url()setsdnssec_valid = falseunless DNSSEC is both available and valid, but still returns parsed OpenAlias addresses from TXT records.
monero-gui/js/TxUtils.js:82-105- For
isDnssecValid === "false"andisAddressValid,handleOpenAliasResolution()returns{ address: resolvedAddress, message: "..." }instead of withholding the address.
- For
monero-gui/pages/Transfer.qml:430-438- The Transfer page shows the warning, then unconditionally writes
response.addressinto the recipient field.
- The Transfer page shows the warning, then unconditionally writes
monero-gui/pages/AddressBook.qml:389-397- The Address Book page has the same pattern and persists the spoofable address if the user saves the entry.
Relevant CLI comparison:
monero/src/simplewallet/simplewallet.cpp:501-534- The CLI path prompts for explicit confirmation and displays DNSSEC status before returning the address. The GUI path does not require a confirm/deny decision before writing the address into the payment form.
Steps to Reproduce
The lab PoC below does not query live DNS, does not open a wallet, does not relay transactions, and does not touch the Monero network. It executes the actual monero-gui/js/TxUtils.js source with mocked QML globals and then applies the same recipient-field assignment performed by Transfer.qml.
- On the lab VM, run the PoC Script:
Code
#!/usr/bin/env node
/*
* Lab-safe reproduction that executes the actual monero-gui/js/TxUtils.js
* handleOpenAliasResolution() function with mocked QML globals.
*
* No DNS queries, wallet actions, or Monero network activity are performed.
*/
const fs = require("fs");
const path = require("path");
const vm = require("vm");
const guiDir = process.env.MONERO_GUI_DIR || "/home/peko/monero/monero-gui";
const txUtilsPath = path.join(guiDir, "js", "TxUtils.js");
const source = fs.readFileSync(txUtilsPath, "utf8");
const attackerAddress =
"45BYNkdWvv45fovzeSgnNdMcZ8Pn9bdnegRyDcggz9XBUbrrgDhwfx63rpvesZMwWEKKQFms81v8hPbCX2eSCb3m9nFk9ZX";
const context = {
console,
qsTr: (text) => text,
appWindow: {
persistentSettings: {
nettype: "mainnet",
},
},
walletManager: {
resolveOpenAlias: () => `false|${attackerAddress}`,
addressValid: (address) => address === attackerAddress,
},
};
vm.createContext(context);
vm.runInContext(source, context, { filename: txUtilsPath });
const openAlias = "donate.getmonero.org";
let recipientField = openAlias;
const response = context.handleOpenAliasResolution(recipientField, "");
console.log(`source_file=${txUtilsPath}`);
console.log("resolver_response=", response);
if (response.message) {
console.log("warning_shown=", response.message);
}
// Transfer.qml lines 430-438 assign response.address even when response.message exists.
if (response.address) {
recipientField = response.address;
}
console.log("recipient_after_resolve=", recipientField);
if (recipientField !== attackerAddress || !response.message || !response.address) {
throw new Error("PoC did not reproduce the vulnerable autofill state");
}
console.log(
"RESULT: vulnerable autofill reproduced with actual TxUtils.js: invalid-DNSSEC OpenAlias still overwrote the recipient address"
);
node /home/peko/reports/monero/poc_gui_openalias_dnssec_autofill_actual_js.js
- If the saved report directory is not present on the lab VM, run the same actual-source harness inline:
cd /home/peko/monero/monero-gui
node - <<'JS'
const fs = require("fs");
const vm = require("vm");
const source = fs.readFileSync("js/TxUtils.js", "utf8");
const attackerAddress = "45BYNkdWvv45fovzeSgnNdMcZ8Pn9bdnegRyDcggz9XBUbrrgDhwfx63rpvesZMwWEKKQFms81v8hPbCX2eSCb3m9nFk9ZX";
const context = {
console,
qsTr: (text) => text,
appWindow: { persistentSettings: { nettype: "mainnet" } },
walletManager: {
resolveOpenAlias: () => `false|${attackerAddress}`,
addressValid: (address) => address === attackerAddress,
},
};
vm.createContext(context);
vm.runInContext(source, context, { filename: "js/TxUtils.js" });
let recipientField = "donate.getmonero.org";
const response = context.handleOpenAliasResolution(recipientField, "");
console.log("resolver_response=", response);
if (response.message) console.log("warning_shown=", response.message);
if (response.address) recipientField = response.address;
console.log("recipient_after_resolve=", recipientField);
if (recipientField !== attackerAddress || !response.message || !response.address) process.exit(1);
console.log("RESULT: vulnerable autofill reproduced with actual TxUtils.js");
JS
- Expected output:
source_file=/home/peko/monero/monero-gui/js/TxUtils.js
resolver_response= {
address: '45BYNkdWvv45fovzeSgnNdMcZ8Pn9bdnegRyDcggz9XBUbrrgDhwfx63rpvesZMwWEKKQFms81v8hPbCX2eSCb3m9nFk9ZX',
message: 'Address found, but the DNSSEC signatures could not be verified, so this address may be spoofed'
}
warning_shown= Address found, but the DNSSEC signatures could not be verified, so this address may be spoofed
recipient_after_resolve= 45BYNkdWvv45fovzeSgnNdMcZ8Pn9bdnegRyDcggz9XBUbrrgDhwfx63rpvesZMwWEKKQFms81v8hPbCX2eSCb3m9nFk9ZX
RESULT: vulnerable autofill reproduced with actual TxUtils.js: invalid-DNSSEC OpenAlias still overwrote the recipient address
{F6137878}
- Code-flow verification:
nl -ba js/TxUtils.js | sed -n '82,105p'
nl -ba pages/Transfer.qml | sed -n '430,438p'
nl -ba pages/AddressBook.qml | sed -n '389,397p'
{F6137884}
This PoC intentionally stops at deterministic source-code execution and QML flow verification. No live desktop GUI, DNS server, wallet, or transaction was used.
Exploit Scenario
- The victim intends to pay
merchant.exampleormerchant@examplethrough OpenAlias. - The attacker controls the victim's DNS resolver, poisons DNS on the local network, or otherwise causes the OpenAlias TXT lookup to return
oa1:xmr recipient_address=<attacker address>;without valid DNSSEC. - Monero GUI receives
false|<attacker address>fromWalletManager::resolveOpenAlias(). TxUtils.handleOpenAliasResolution()returns both a spoofing warning andaddress: <attacker address>.Transfer.qmlshows the warning but still overwrites the recipient field with the attacker address.- If the victim continues through the normal send confirmation flow, the transaction pays the attacker address.
Fix Recommendation
Make invalid or unavailable DNSSEC a hard stop for automatic GUI autofill.
Preferred code-level fix in monero-gui/js/TxUtils.js:
} else if (isDnssecValid === "false") {
if (isAddressValid) {
return {
message: qsTr("Address found, but the DNSSEC signatures could not be verified, so this address may be spoofed"),
};
} else {
return { message: qsTr("No valid address found at this OpenAlias address, but the DNSSEC signatures could not be verified, so this may be spoofed") };
}
}
Additional hardening:
- In
Transfer.qmlandAddressBook.qml, only assignresponse.addresswhen the response indicates authenticated DNSSEC success. Do not infer safety from the address being syntactically valid. - If product requirements need unsigned OpenAlias support, show a blocking confirmation dialog with the full OpenAlias name, full resolved address, DNSSEC failure state, and a default-cancel action. Only after explicit confirmation should the GUI fill the address.
- Consider changing
WalletManager::resolveOpenAlias()to return structured fields instead of a pipe-delimited string, for example{dnssec_valid, address, error}. This reduces the chance that UI code accidentally treatsfalse|addressas an acceptable address result. - Add regression tests for
handleOpenAliasResolution():true|validAddressreturns an address.false|validAddressreturns only a warning and does not mutate the recipient field.false|invalidAddressreturns only a warning/error.
CWE Classification
- CWE-345: Insufficient Verification of Data Authenticity
- The GUI consumes address data from DNS/OpenAlias even when DNSSEC authenticity validation failed. A warning is shown, but the unauthenticated address is still accepted into the transaction recipient field.
- CWE-20: Improper Input Validation
- The code validates only that the resolved string is a syntactically valid Monero address. It does not enforce the security property required for OpenAlias payment safety: authenticated binding between the human-readable alias and the resolved address.
Note
The PoC script poc_gui_openalias_dnssec_autofill_actual_js.js was written with AI assistance for the mocked QML globals and lab-safe source execution harness. The harness executes the actual monero-gui/js/TxUtils.js file from the checked-out source tree. The vulnerable code path, line references, and the decision that this is separate from the already-submitted GUI price-fetch TLS and transaction-details rich-text issues were verified by reading the source directly. The PoC was executed only on the provided lab VM and did not perform active exploitation against Monero networks or live Monero Project services.
Bounty
If this qualifies for a payout, XMR can be sent to:
45BYNkdWvv45fovzeSgnNdMcZ8Pn9bdnegRyDcggz9XBUbrrgDhwfx63rpvesZMwWEKKQFms81v8hPbCX2eSCb3m9nFk9ZX
Impact
An attacker who can influence a victim’s OpenAlias DNS lookup can cause Monero GUI to place an attacker-controlled Monero address into the Transfer recipient field even when DNSSEC validation fails.
The GUI does show a warning, but it still accepts and applies the unauthenticated address. This creates a direct fund-loss risk: if the victim continues with the normal send flow, the transaction is constructed for the attacker-controlled address, and the XMR transfer is irreversible.
Practical attack scenarios include:
- Malicious or compromised DNS resolver returns an attacker OpenAlias TXT record.
- Local network attacker poisons or intercepts DNS responses.
- Attacker-controlled OpenAlias/domain configuration has no valid DNSSEC but still returns a valid Monero address.
- Victim relies on the GUI’s Resolve behavior and does not manually compare the final long address.
Security impact:
- Unauthenticated DNS data can alter the payment recipient.
- Human-readable recipient intent is broken: merchant.example can resolve into attacker address.
- Funds can be permanently lost if the user submits the transaction.
- Address Book entries can also persist the spoofed address, making future payments to the same contact unsafe.