bitcoinjs-lib accepte des blocs forgés (CVE-2026-107846)
bitcoinjs-lib jusqu'à 7.0.1 accepte une liste de transactions forgée sous un véritable identifiant de bloc ; corrigé dans 5.2.1, 6.1.8 et 7.0.2.
En bref
- bitcoinjs-lib valide un bloc forgé dont la liste de transactions diffère du bloc réel. Un pair duplique la dernière transaction pour combler le niveau impair de l'arbre de Merkle, et
checkTxRoots()renvoie toujours vrai. - Les versions 2.3.0 à 5.2.0, 6.0.0 à 6.1.7 et 7.0.0-rc.0 à 7.0.1 sont concernées. La faille touche la validation des blocs, pas la signature des transactions.
- L'enregistrement CVE la note CVSS v4.0 9,3 Critique, tandis que l'avis du dépôt la note élevée. Une preuve de concept publique figure dans l'avis ; aucune exploitation n'est connue, et elle n'est pas dans le catalogue KEV de la CISA au 2026-11-09.
- Mettez à niveau vers 5.2.1, 6.1.8 ou 7.0.2. Si vous validez des blocs non fiables, rejetez aussi tout bloc qui répète un identifiant de transaction.
Suis-je concerné ?
Toutes ces conditions doivent être réunies :
- Vous utilisez
bitcoinjs-liben version 2.3.0 à 5.2.0, 6.0.0 à 6.1.7, ou 7.0.0-rc.0 à 7.0.1. - Vous analysez des blocs provenant d'une source non fiable et vous agissez sur le résultat de
Block.checkTxRoots(),Block.calculateMerkleRoot()ou de l'identifiant du bloc. - Une mauvaise réponse a une conséquence dans votre système, comme créditer un dépôt, indexer deux fois une transaction ou accepter un bloc qu'un autre nœud rejette.
Le code concerné est le validateur de blocs ajouté par la bibliothèque en 2.3.0 sous le nom Block.checkMerkleRoot(), puis exposé sous le nom Block.checkTxRoots(). Le code qui ne fait que construire ou signer des transactions ne l'appelle pas. Un portefeuille qui n'analyse jamais un bloc reçu d'un pair n'est pas exposé par ce chemin.
| Branche | Versions concernées | Version corrigée |
|---|---|---|
| 7.x | 7.0.0-rc.0 à 7.0.1 | 7.0.2 |
| 6.x | 6.0.0 à 6.1.7, y compris 6.1.0-rc.0 | 6.1.8 |
| 5.x et antérieures | 2.3.0 à 5.2.0 | 5.2.1 |
| Avant 2.3.0 | Aucun vérificateur de racine de bloc | Non concerné |
Le dépôt a publié cinq autres avis le 2026-10-26 : une sous-estimation des frais PSBT, deux problèmes de déni de service à l'analyse, l'acceptation d'une cible compacte invalide et un bogue de frais lié à des UTXO contradictoires. Ce sont des failles distinctes avec leurs propres versions corrigées, plus récentes que celles de ce CVE : 5.2.3, 6.1.10 et 7.0.5, que npm liste comme versions de maintenance et dernière version actuelles.
Vérifiez ce qui est installé :
npm ls bitcoinjs-lib
node -p "require('bitcoinjs-lib/package.json').version"
grep -n '"node_modules/bitcoinjs-lib"' -A 4 package-lock.json
grep -n "bitcoinjs-lib" yarn.lock pnpm-lock.yaml
Cherchez ensuite les appels qui comptent :
grep -rn "checkTxRoots\|calculateMerkleRoot\|checkMerkleRoot" --include="*.js" --include="*.ts" .
Comment corriger ?
Mettez à niveau vers la version corrigée de votre branche : 5.2.1, 6.1.8 ou 7.0.2. Les trois sont sorties le 2026-09-07, sept semaines avant la publication de l'avis.
- Pour la branche 7.x :
npm install bitcoinjs-lib@7.0.2
- Pour la branche 6.x :
npm install bitcoinjs-lib@6.1.8
- Pour la branche 5.x :
npm install bitcoinjs-lib@5.2.1
- Les versions antérieures à 5.0.0 n'ont pas de correctif intégré. Passez à une branche corrigée, ou ajoutez le contrôle applicatif de cette section avant de faire confiance à un bloc.
Si vous ne pouvez pas mettre à niveau immédiatement, rejetez tout bloc dont la liste de transactions répète un identifiant de transaction avant d'appeler checkTxRoots(). Un identifiant répété est la forme observable de la mutation, et il est plus simple à auditer que la condition interne de remplissage.
// Application-level guard, adapted from the advisory's remediation note.
function hasRepeatedTxid(block) {
const ids = block.transactions.map(tx => tx.getId());
return new Set(ids).size !== ids.length;
}
if (!hasRepeatedTxid(block) && block.checkTxRoots()) {
// accept the block
}
Ce contrôle couvre la forme de l'attaque par transaction dupliquée. Il ne transforme pas la bibliothèque en validateur complet de blocs et ne remplace pas la mise à niveau.
Quelle est la cause racine ?
La routine de racine de Merkle complète un niveau impair en hachant le dernier nœud avec lui-même, puis renvoie la racine sans signaler ce remplissage. La construction de l'arbre de Merkle de Bitcoin fait le même remplissage. Bitcoin Core analyse aussi chaque niveau pour trouver deux hachages adjacents égaux et lève un indicateur mutated, que CheckBlock transforme en rejet bad-txns-duplicate. bitcoinjs-lib effectuait le remplissage et perdait le signal.
Dans ts_src/merkle.ts à la version v7.0.1, fastMerkleRoot duplique le dernier nœud lorsqu'un niveau a une longueur impaire :
let length = values.length;
const results = values.concat();
while (length > 1) {
let j = 0;
for (let i = 0; i < length; i += 2, ++j) {
const left = results[i];
const right = i + 1 === length ? left : results[i + 1];
const data = tools.concat([left, right]);
results[j] = digestFn(data);
}
length = j;
}
return results[0];
Lorsque i + 1 === length, right prend la valeur de left, donc le dernier nœud est haché avec lui-même. Il n'y a ni comparaison avec le nœud précédent ni paramètre de sortie pour signaler un doublon. La fonction ne renvoie que la racine.
Block.calculateMerkleRoot transmet cette racine sans changement, et Block.__checkMerkleRoot la compare au seul champ d'en-tête :
private __checkMerkleRoot(): boolean {
if (!this.transactions) throw errorMerkleNoTxes;
const actualMerkleRoot = Block.calculateMerkleRoot(this.transactions);
return tools.compare(this.merkleRoot!, actualMerkleRoot) === 0;
}
checkTxRoots() ajoute le contrôle du witness commit, puis appelle __checkMerkleRoot(). Un en-tête qui valide la vraie racine passe donc, même lorsque la liste de transactions porte une copie supplémentaire de la dernière transaction absorbée par le remplissage. L'attaquant n'a pas besoin de miner : l'en-tête, la preuve de travail et l'identifiant du bloc restent identiques.
Un identifiant de bloc ne hache que l'en-tête de 80 octets, et l'en-tête n'authentifie la liste de transactions qu'à travers la racine de Merkle. Tout changement de la liste qui préserve la racine préserve donc l'identifiant et toutes les recherches qui s'appuient sur lui.
La même classe de duplication est CVE-2012-2459, la mutation corrigée par Bitcoin Core en 2012. L'avis indique que Core rejette le cas via ComputeMerkleRoot et CheckBlock. C'est un écart de parité dans la bibliothèque, pas une nouvelle faille de construction dans Bitcoin.
Comment la faille est-elle exploitée ?
L'attaque reprend un bloc existant et change sa liste de transactions sans changer son en-tête :
- L'attaquant prend un bloc réel de la chaîne, avec un en-tête valide et une preuve de travail, dont l'arbre de Merkle a un nombre impair de nœuds à un niveau.
- L'attaquant analyse le bloc, ajoute une copie de la dernière transaction à la liste et le sérialise à nouveau. Cela ajoute 259 octets dans l'exemple de la chaîne principale de l'avis et laisse l'en-tête intact.
- L'attaquant relaie le bloc muté à un service qui analyse les blocs avec bitcoinjs-lib et s'appuie sur
checkTxRoots(). - Le service recalcule la racine de Merkle. La transaction dupliquée vient combler le niveau impair, donc la racine et l'identifiant du bloc correspondent à l'en-tête, et
checkTxRoots()renvoie vrai. - Le service voit maintenant un bloc avec une liste de transactions différente sous un identifiant qui appartient aussi au bloc honnête. Un indexeur peut créditer deux fois la transaction dupliquée, ou un système de dépôt peut la compter deux fois.
L'avis note que la plupart des blocs réels atteignent un nombre impair de nœuds à un niveau, donc la plupart des blocs existants peuvent être réécrits.
Sa preuve de concept s'exécute sur un bloc réel de la chaîne principale, issu du jeu de fixtures Bitcoin Core du dépôt. Le bloc honnête a neuf transactions et valide ; la copie forgée en a dix, 259 octets de plus et le même identifiant de bloc, et elle valide aussi. L'avis étend également le même bloc à douze transactions avec le même résultat.
// Adapted from the advisory's proof of concept, trimmed to the mutation.
const honest = Block.fromHex(fixture.hex);
const forged = Block.fromHex(fixture.hex);
forged.transactions = forged.transactions.concat([
forged.transactions[forged.transactions.length - 1],
]);
const parsed = Block.fromHex(forged.toHex());
honest.getId() === parsed.getId(); // true
parsed.checkTxRoots(); // true on v7.0.1
Comment se déroule l'attaque ?
Un pair réécrit un bloc existant en dupliquant sa dernière transaction. Le calcul de racine de la bibliothèque accepte la liste réécrite parce qu'il ne signale jamais le remplissage qui a rendu le doublon invisible.
flowchart LR
A["Un pair relaie un bloc muté"] --> B["Block.fromHex analyse les transactions dupliquées"]
B --> C["fastMerkleRoot complète le niveau impair"]
C --> D["aucun contrôle des paires dupliquées"]
D --> E["checkTxRoots renvoie vrai"]
E --> F["L'indexeur crédite la transaction dupliquée"]
Que peut faire un attaquant ?
L'impact se situe chez le consommateur, pas dans la bibliothèque. Avec un bloc forgé accepté, un attaquant peut :
- Faire créditer deux fois la même transaction à un service qui crédite des transactions, lorsque ce service utilise
checkTxRoots()comme contrôle d'intégrité des blocs. - Faire stocker à un explorateur ou à un indexeur une liste de transactions qui ne correspond pas à la chaîne, sous un identifiant de bloc qui, lui, correspond.
- Déclencher le déni de service décrit dans CVE-2012-2459 : un consommateur qui réagit en mettant l'identifiant du bloc sur liste noire rejette aussi le bloc honnête portant cet identifiant.
L'attaque exige un consommateur qui analyse des blocs non fiables et agit sur checkTxRoots(). Elle ne récupère pas de clés privées, ne signe pas de transactions et ne permet pas à l'attaquant de dépenser directement les pièces d'autrui. L'avis précise que la gravité dépend du consommateur, car la validation des blocs est une part plus faible de l'usage de cette bibliothèque que le traitement des transactions.
Un service qui ne fait que signer ou diffuser des transactions n'est pas la cible. L'exposition se situe sur le chemin de lecture : les indexeurs, les plateformes d'échange, les explorateurs et tout pipeline de dépôt qui accepte un bloc parce que la bibliothèque a dit que sa racine de transactions correspondait.
Pourquoi le correctif fonctionne-t-il ?
Le correctif signale le cas de remplissage hors du calcul de racine et fait échouer la validation du bloc lorsqu'une véritable paire de doublons est trouvée. Le commit b79c7714c0687473dc6e25c69fc5fc597302c7a0 ajoute un paramètre de sortie mutated à fastMerkleRoot et le vérifie dans __checkMerkleRoot.
export function fastMerkleRoot(
values: Uint8Array[],
digestFn: (b: Uint8Array) => Uint8Array,
+ mutated?: { value: boolean },
): Uint8Array {
...
let length = values.length;
const results = values.concat();
+ if (mutated) mutated.value = false;
while (length > 1) {
let j = 0;
for (let i = 0; i < length; i += 2, ++j) {
const left = results[i];
const right = i + 1 === length ? left : results[i + 1];
+ // Odd-node padding is not a mutation; only compare pairs from this level.
+ if (mutated && i + 1 < length && tools.compare(left, right) === 0)
+ mutated.value = true;
const data = tools.concat([left, right]);
results[j] = digestFn(data);
}
La condition i + 1 < length est la partie importante. Elle compare deux hachages réellement frères à ce niveau, et elle exclut la paire synthétique créée par le remplissage du niveau impair. Une valeur seule n'est pas signalée, et deux hachages égaux situés dans des paires différentes ne le sont pas non plus, ce que couvrent les nouveaux tests du commit.
__checkMerkleRoot renvoie ensuite faux lorsque l'indicateur est levé :
private __checkMerkleRoot(): boolean {
if (!this.transactions) throw errorMerkleNoTxes;
- const actualMerkleRoot = Block.calculateMerkleRoot(this.transactions);
- return tools.compare(this.merkleRoot!, actualMerkleRoot) === 0;
+ const mutated = { value: false };
+ const actualMerkleRoot = Block.calculateMerkleRoot(
+ this.transactions,
+ false,
+ mutated,
+ );
+ return (
+ !mutated.value && tools.compare(this.merkleRoot!, actualMerkleRoot) === 0
+ );
}
Le correctif existe dans les trois branches corrigées : le commit b79c7714c0687473dc6e25c69fc5fc597302c7a0 pour la 7.x, 009f165edfbf925cad8964b2dce478d18dd19449 pour la 6.x, et f3434d1e4bb5e0784fe769c4cd08a243576232a1 pour la 5.2.1, qui remplace aussi la dépendance externe merkle-lib par une implémentation locale. Le correctif est complet pour le chemin de racine de bloc de la bibliothèque. Il n'ajoute pas de contrôle des transactions répétées au code de Merkle de chaque appelant, ce qui explique pourquoi le contrôle applicatif reste utile.
Comment vérifier si j'ai été compromis ?
Il n'y a pas ici de compromission d'hôte à rechercher : l'attaque change les blocs qu'un consommateur accepte, donc les preuves sont dans les enregistrements de blocs et de dépôts. Cherchez les blocs dont la liste de transactions stockée diffère d'une seconde source, et les identifiants de transaction répétés dans un même bloc.
- Comparez les blocs stockés à un second nœud Bitcoin ou à un explorateur pour la même hauteur et le même identifiant de bloc.
- Recherchez un bloc dont la liste de transactions contient un
txiden double. - Auditez les crédits et les dépôts fondés sur un bloc validé par l'ancienne bibliothèque.
- Relancez
checkTxRoots()avec une version corrigée sur les blocs stockés et notez tout bloc qu'elle rejette désormais.
# List transaction IDs in a stored block and find duplicates with jq.
jq -r '.tx[] | .txid' stored-block.json | sort | uniq -d
# Compare the block ID your service recorded with a second node.
bitcoin-cli getblockhash <height>
bitcoin-cli getblock <blockhash> 2
Un bloc qui échoue à la comparaison avec la seconde source est un élément à examiner. Un bloc qui correspond ne prouve pas que votre consommateur l'a traité correctement, car la faille est dans le chemin de validation du consommateur, pas dans la chaîne.
Chronologie
- 2026-09-07 : les correctifs sortent dans 5.2.1, 6.1.8 et 7.0.2.
- 2026-10-08 : le CVE est réservé.
- 2026-10-26 : le dépôt publie six avis, dont GHSA-8vv3-6qpc-gpw6.
- 2026-11-04 : l'enregistrement CVE de CVE-2026-107846 est publié.
FAQ
Cette faille peut-elle voler des bitcoins dans un portefeuille ?
Non, pas à elle seule. Elle n'expose pas de clés et ne crée pas de signatures valides. Le risque de vol est indirect : un service qui crédite des dépôts à partir d'un bloc validé avec l'ancienne bibliothèque peut être amené à créditer deux fois la même transaction.
Bitcoin Core accepte-t-il le bloc forgé ?
Non. L'avis indique que Bitcoin Core détecte la mutation dans ComputeMerkleRoot et rejette le bloc avec bad-txns-duplicate. La faille est une différence entre le validateur de blocs de bitcoinjs-lib et le comportement de Core.
Dois-je mettre à niveau si je ne fais que créer des transactions ?
Pas pour cette faille, sauf si votre code analyse aussi des blocs et appelle checkTxRoots(). La construction et la signature de transactions ne passent pas par le chemin de racine de bloc concerné.
Pourquoi le bloc forgé garde-t-il le même identifiant ?
L'identifiant du bloc est le hachage de l'en-tête, et l'en-tête ne change pas. La transaction dupliquée ne change que la liste de transactions et la taille sérialisée du bloc, qui se trouvent hors du hachage de l'en-tête.
Existe-t-il une solution de contournement si je ne peux pas mettre à niveau ?
Oui, pour la forme de la transaction dupliquée. Rejetez tout bloc qui répète un identifiant de transaction avant de faire confiance à checkTxRoots(). Le contrôle figure dans « Comment corriger ? » et doit être traité comme temporaire jusqu'à la mise à niveau.
Trouver les copies obsolètes de bitcoinjs-lib avec Ostorlab
L'analyse de composition logicielle et l'inventaire SBOM d'Ostorlab enregistrent les versions de bibliothèques comme bitcoinjs-lib dans vos dépôts et images de conteneurs. Cela vous permet de trouver les services qui résolvent encore une version antérieure à 5.2.1, 6.1.8 ou 7.0.2.
Références
- Avis du dépôt GHSA-8vv3-6qpc-gpw6, « Undetected merkle node duplication in block root computation validates a forged transaction list » (2026-10-26) : https://github.com/bitcoinjs/bitcoinjs-lib/security/advisories/GHSA-8vv3-6qpc-gpw6 (vérifié le 9 novembre 2026)
- Enregistrement CVE CVE-2026-107846 (2026-11-04) : https://www.cve.org/CVERecord?id=CVE-2026-107846 (vérifié le 9 novembre 2026)
- Commit de correction b79c7714c0687473dc6e25c69fc5fc597302c7a0 (2026-09-03) : https://github.com/bitcoinjs/bitcoinjs-lib/commit/b79c7714c0687473dc6e25c69fc5fc597302c7a0 (vérifié le 9 novembre 2026)
- Commit de correction 009f165edfbf925cad8964b2dce478d18dd19449 pour la branche 6.x (2026-09-03) : https://github.com/bitcoinjs/bitcoinjs-lib/commit/009f165edfbf925cad8964b2dce478d18dd19449 (vérifié le 9 novembre 2026)
- Commit de correction f3434d1e4bb5e0784fe769c4cd08a243576232a1 pour la 5.2.1 (2026-09-07) : https://github.com/bitcoinjs/bitcoinjs-lib/commit/f3434d1e4bb5e0784fe769c4cd08a243576232a1 (vérifié le 9 novembre 2026)
- CVE-2012-2459, la classe de mutation de l'arbre de Merkle (vérifié le 9 novembre 2026) : https://www.cve.org/CVERecord?id=CVE-2012-2459
- Nombre de téléchargements npm de bitcoinjs-lib, du 2 au 8 novembre 2026 : https://api.npmjs.org/downloads/point/last-week/bitcoinjs-lib (vérifié le 9 novembre 2026)