test: characterize walletnotify shell injection

`-walletnotify` shell-escapes wallet names before substituting `%w` into the configured command.
`ReplaceAll()` uses `%w` as the regex pattern and the escaped wallet name as replacement text, where `$'` copies the command suffix into the escaped name and allows its shell metacharacters to alter the command.

Record the command execution, missing notification file, regex pattern matching, replacement expansion, and non-recursive replacement.
This commit is contained in:
Lőrinc
2026-08-15 12:10:07 -07:00
parent 4efaa6763a
commit 604d7e8fdd
2 changed files with 18 additions and 0 deletions

View File

@@ -42,6 +42,7 @@ class NotificationsTest(BitcoinTestFramework):
self.num_nodes = 2
self.setup_clean_chain = True
self.uses_wallet = None
self.noban_tx_relay = True
def setup_network(self):
self.wallet = ''.join(chr(i) for i in range(FILE_CHAR_START, FILE_CHAR_END) if chr(i) not in FILE_CHARS_DISALLOWED)
@@ -175,6 +176,19 @@ class NotificationsTest(BitcoinTestFramework):
self.expect_wallet_notify([(bump2, blockheight2, blockhash2), (tx2, -1, UNCONFIRMED_HASH_STRING)])
assert_equal(self.nodes[1].gettransaction(bump2)["confirmations"], 1)
if platform.system() != 'Windows':
self.log.info("test -walletnotify replacement metacharacters in wallet name")
self.nodes[1].unloadwallet(self.wallet)
command_marker = os.path.join(self.options.tmpdir, "walletnotify_injected")
# The previous regex replacement expanded `$'` to the command suffix, breaking the shell-escaped wallet name's quote accounting
wallet_name = self.nodes[1].createwallet(f"$'$'; echo Pwned > {os.path.basename(command_marker)}; #")["name"]
txid = self.nodes[0].sendtoaddress(self.nodes[1].get_wallet_rpc(wallet_name).getnewaddress(), 1)
self.sync_mempools()
notify_path = os.path.join(self.walletnotify_dir, notify_outputname(wallet_name, txid))
self.wait_until(lambda: os.path.exists(command_marker) or os.path.exists(notify_path), timeout=10)
assert os.path.exists(command_marker) # TODO: Wallet names must not inject shell commands.
assert not os.path.exists(notify_path) # TODO: Wallet names must remain literal in notification paths.
self.log.info("test -alertnotify with large work invalid chain")
# create a bunch of invalid blocks
tip = self.nodes[0].getbestblockhash()