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.
