News

A wallet name that could run commands

Bitcoin Core quoted the wallet name before pasting it into the -walletnotify command, and then the pasting step read the quoted text again as a regex format string. The fix merged on 2 September 2026 and is in no release yet.

4 min readBitcoin Core
A wallet name that could run commands

What happened

On 2 September 2026 Bitcoin Core merged pull request 36048, "util: keep wallet names literal in notification commands". The release note the patch adds says it plainly: "On non-Windows systems, an authenticated RPC caller allowed to create wallets could execute arbitrary commands as the node process account when -walletnotify was configured, by crafting a wallet name with regex replacement characters." The change is on master, targeted at version 32.0, and no release carries it.

What it changes

-walletnotify is an option on a node you run that executes a shell command every time a wallet transaction arrives. The command is a template, and Core substitutes placeholders into it before running it: %s for the transaction id, %b for the block hash, %h for the height, %w for the name of the wallet. Wallet names are chosen by whoever calls createwallet, so Core already treated them as hostile input and quoted them first. The line in wallet.cpp reads ReplaceAll(strCmd, "%w", ShellEscape(GetName())).

The quoting was never the problem. The step after it read the quoted text a second time, in a different language. Since pull request 25803, merged on 16 August 2022 to drop a Boost dependency, ReplaceAll had been a one-line call to std::regex_replace. In that function the replacement argument is not a literal string. It is a format string, in which $& stands for the matched text, $` for everything before the match, and $' for everything after it. A wallet name carrying $' therefore expanded, at substitution time, into a copy of the rest of the command line, pasted back inside the quotes ShellEscape had just put around it. Once the quote accounting is off, the attacker's text stops being an argument to the command and starts being a command.

The patch throws the regex out for a plain scan: find, append the replacement, advance past it, never look at it again. The function's new comment states the property that was missing, that both arguments are treated "literally; the replacement text is not searched again", and the unit tests gained a case that feeds every regex format character through at once.

What it does not change

No released binary is fixed. The std::regex_replace version shipped in v24.0 in November 2022 and is still in v31.1 of 8 July 2026. Until 32.0, an operator who wants this fix builds from master.

Nor is it something a peer can do to you. It requires an authenticated caller on the RPC interface who is allowed to create wallets, and an operator who configured -walletnotify in the first place. Nothing here is reachable over the peer-to-peer network, and the release note excludes Windows. What it did was change what a wallet-creation permission is worth: on a node where several people or several services hold RPC credentials, the ability to name a wallet was quietly the ability to run code as the account the node runs under, which is the account that can read every wallet file on the machine.

It is also not the whole of the wallet-name question. A separate change, pull request 35833, rejects control characters in new wallet names, because those forge log lines and hide inside paths. Neither change makes a wallet name a safe thing to hand to something else unexamined.

Context

Core publishes fixes like this one in the open rather than as an advisory, which is the choice its disclosure policy describes: severity decides the embargo, and a bug that needs authenticated access is not treated as a network emergency. The same month produced a wallet key derivation count that could go negative, another case of a value from a file being trusted a little further than it had been checked.

The transferable part is about where escaping stops working. Escaping is always relative to one consumer: ShellEscape makes text safe for a shell, and nothing else. Put a second interpreter between the escaping and the shell, and the guarantee is gone, because the second one has its own metacharacters and has never heard of the first one's. Here that second interpreter arrived in 2022 as a dependency cleanup, with the same function name and the same signature at every call site, and it took four years for someone to ask what std::regex_replace does with a dollar sign.

Newsletter

Bitcoin, without the noise

What happened in Bitcoin, what it actually changes, and the sources so you can check us. One issue at a time, straight to your inbox.

  • One email per issue, never a drip campaign
  • No tracking pixels and no shared addresses
  • Unsubscribe from any issue in one click

Get the next issue

One email per issue, no tracking pixels, and unsubscribe from any of them. We do not share your address. Privacy policy