Qué pasó
El 2 de septiembre de 2026 Bitcoin Core integró la
pull request 36048, "util: keep wallet
names literal in notification commands". La
nota de versión que añade el parche
lo dice sin rodeos: "En sistemas que no son Windows, quien llamara al RPC de forma
autenticada y tuviera permiso para crear wallets podía ejecutar comandos arbitrarios con la
cuenta del proceso del nodo cuando -walletnotify estaba configurado, construyendo un
nombre de wallet con caracteres de sustitución de expresiones regulares". El cambio está en
master, apunta a la versión 32.0 y ninguna versión publicada lo lleva.
Qué cambia
-walletnotify es una opción de un nodo propio que ejecuta un
comando de shell cada vez que llega una transacción de la wallet. El comando es una
plantilla, y Core sustituye marcadores en ella antes de ejecutarla: %s por el
identificador de la transacción, %b por el hash del bloque, %h por la altura y %w por
el nombre de la wallet. Los nombres de wallet los elige quien llama a createwallet, así
que Core ya los trataba como entrada hostil y los entrecomillaba primero. La línea de
wallet.cpp
dice ReplaceAll(strCmd, "%w", ShellEscape(GetName())).
El entrecomillado nunca fue el problema. El paso siguiente volvía a leer ese texto ya
entrecomillado, en otro idioma. Desde la
pull request 25803, integrada el 16 de
agosto de 2022 para quitar una dependencia de Boost, ReplaceAll era una única llamada a
std::regex_replace. En esa función el argumento de reemplazo no es una cadena literal. Es
una cadena de formato, en la que $& representa el texto que hizo coincidencia,
$` todo lo anterior a la coincidencia y $' todo lo posterior. Un nombre de wallet
que llevara $' se expandía entonces, en el momento de la sustitución, a una copia del
resto de la línea de comandos, pegada dentro de las comillas que ShellEscape acababa de
poner a su alrededor. Una vez que las comillas dejan de cuadrar, el texto del atacante deja
de ser un argumento del comando y pasa a ser un comando.
El parche tira la expresión regular y pone un recorrido llano: buscar, añadir el reemplazo, avanzar más allá de él y no volver a mirarlo. El nuevo comentario de la función enuncia la propiedad que faltaba, que ambos argumentos se tratan "literalmente; el texto de reemplazo no se vuelve a buscar", y las pruebas unitarias ganaron un caso que le pasa de golpe todos los caracteres de formato de expresiones regulares.
Qué no cambia
Ningún binario publicado está corregido. La versión con std::regex_replace llegó en
v24.0 en noviembre de
2022 y sigue en
v31.1, del 8 de julio
de 2026. Hasta la 32.0, quien quiera esta corrección hoy compila desde master.
Tampoco es algo que un peer pueda hacerle a nadie. Hace falta una llamada autenticada a la
interfaz RPC con permiso para crear wallets, y un operador que haya configurado
-walletnotify en primer lugar. Nada de esto es alcanzable por la red peer-to-peer, y la
nota de versión excluye Windows. Lo que sí cambió es cuánto vale un permiso de creación de
wallets: en un nodo donde varias personas o varios servicios tienen credenciales RPC, poder
poner nombre a una wallet era en silencio poder ejecutar código con la cuenta bajo la que
corre el nodo, que es la cuenta que puede leer todos los archivos de wallet de la máquina.
Tampoco agota la cuestión de los nombres de wallet. Un cambio aparte, la pull request 35833, rechaza caracteres de control en los nombres de wallet nuevos, porque falsifican líneas de registro y se esconden dentro de las rutas. Ninguno de los dos cambios convierte un nombre de wallet en algo seguro de entregar a otra cosa sin examinarlo.
Contexto
Core publica correcciones como esta a la vista de todos en lugar de como aviso de seguridad, que es la elección que describe su política de divulgación: la gravedad decide el embargo, y un fallo que exige acceso autenticado no se trata como una emergencia de red. El mismo mes produjo un contador de derivación de claves que podía volverse negativo, otro caso de un valor leído de un archivo en el que se confió un poco más de lo que se había comprobado.
La parte transferible trata de dónde deja de funcionar el escapado. El escapado siempre es
relativo a un consumidor: ShellEscape hace que un texto sea seguro para un shell, y para
nada más. Si se pone un segundo intérprete entre el escapado y el shell, la garantía
desaparece, porque el segundo tiene sus propios metacaracteres y nunca ha oído hablar de los
del primero. Aquí ese segundo intérprete llegó en 2022 como una limpieza de dependencias,
con el mismo nombre de función y la misma firma en todos los sitios donde se llamaba, y
pasaron cuatro años hasta que alguien preguntó qué hace
std::regex_replace con un signo
de dólar.
