bitcoinjs-lib が偽造ブロックを受理 (CVE-2026-107846)
bitcoinjs-lib 7.0.1 以前は実在するブロック ID のまま偽造トランザクション一覧を受理します。5.2.1、6.1.8、7.0.2 で修正済みです。
要点
- bitcoinjs-lib は、実際のブロックとトランザクション一覧が異なる偽造ブロックを検証してしまいます。ピアが最後のトランザクションを複製してマークル木の奇数レベルの欠落ノードを埋めると、
checkTxRoots()は true を返します。 - 2.3.0 から 5.2.0、6.0.0 から 6.1.7、7.0.0-rc.0 から 7.0.1 が影響を受けます。この不具合はブロック検証にあり、トランザクション署名にはありません。
- CVE レコードは CVSS v4.0 9.3 の Critical、リポジトリのアドバイザリは high と評価しています。アドバイザリには公開された実証コードがあります。悪用は確認されておらず、2026-11-09 時点で CISA KEV にはありません。
- 5.2.1、6.1.8、7.0.2 に更新してください。信頼できないブロックを検証する場合は、トランザクション ID を繰り返すブロックも拒否してください。
影響を受けますか?
次のすべてが当てはまる場合に影響を受けます。
bitcoinjs-libの 2.3.0 から 5.2.0、6.0.0 から 6.1.7、または 7.0.0-rc.0 から 7.0.1 を使用している。- 信頼できない提供元のブロックを解析し、
Block.checkTxRoots()、Block.calculateMerkleRoot()、またはブロック ID の結果に基づいて処理する。 - 誤った結果がシステムに影響する。たとえば入金の付与、トランザクションの二重索引付け、別ノードが拒否するブロックの受理など。
影響するコードは、ライブラリが 2.3.0 で Block.checkMerkleRoot() として追加し、後に Block.checkTxRoots() として公開したブロック検証器です。トランザクションを構築または署名するだけのコードはこれを呼びません。ピアからブロックを解析しないウォレットは、この経路では影響を受けません。
| ブランチ | 影響を受けるバージョン | 修正バージョン |
|---|---|---|
| 7.x | 7.0.0-rc.0 から 7.0.1 | 7.0.2 |
| 6.x | 6.0.0 から 6.1.7(6.1.0-rc.0 を含む) | 6.1.8 |
| 5.x 以前 | 2.3.0 から 5.2.0 | 5.2.1 |
| 2.3.0 より前 | ブロックルート検証器なし | 影響なし |
リポジトリは 2026-10-26 にさらに 5 件のアドバイザリを公開しました。PSBT 手数料の過小評価、解析時のサービス拒否 2 件、無効なコンパクトターゲットの受理、競合する UTXO による手数料の不具合です。これらは別個の不具合で独自の修正バージョンを持ち、この CVE の修正バージョンより新しく、5.2.3、6.1.10、7.0.5 が npm の現在のメンテナンス版と最新版です。
インストールされているバージョンを確認します。
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
次に重要な呼び出し箇所を探します。
grep -rn "checkTxRoots\|calculateMerkleRoot\|checkMerkleRoot" --include="*.js" --include="*.ts" .
修正方法
使用中のブランチの修正版、5.2.1、6.1.8、7.0.2 に更新してください。3 つはすべて 2026-09-07 に公開され、アドバイザリの公開より 7 週間前でした。
- 7.x ブランチの場合:
npm install bitcoinjs-lib@7.0.2
- 6.x ブランチの場合:
npm install bitcoinjs-lib@6.1.8
- 5.x ブランチの場合:
npm install bitcoinjs-lib@5.2.1
- 5.0.0 より前のバージョンにはインラインの修正がありません。修正済みのブランチへ移行するか、このセクションのアプリケーション側チェックを追加してからブロックを信頼してください。
すぐに更新できない場合は、checkTxRoots() を呼ぶ前に、トランザクション ID を繰り返すブロックを拒否してください。繰り返された ID は改変の観測可能な形であり、内部の詰め物の条件より監査しやすいです。
// 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
}
このチェックはトランザクション複製型の攻撃を対象にします。ライブラリを完全なブロック検証器にするものではなく、更新の代わりにもなりません。
根本原因は何ですか?
マークルルートの処理は、奇数レベルの最後のノードを自身とハッシュして詰め物をし、その詰め物を報告せずにルートを返します。Bitcoin のマークル木の構築も同じ詰め物をします。Bitcoin Core はさらに各レベルで隣接する等しいハッシュの組を調べ、mutated フラグを立て、CheckBlock がそれを bad-txns-duplicate の拒否に変えます。bitcoinjs-lib は詰め物をして、その信号を捨てていました。
v7.0.1 の ts_src/merkle.ts では、fastMerkleRoot がレベル長の奇数のときに最後のノードを複製します。
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];
i + 1 === length のとき、right は left と同じになります。そのため最後のノードは自身とハッシュされます。前のノードとの比較も、複製を報告する出力パラメータもありません。関数はルートだけを返します。
Block.calculateMerkleRoot はそのルートをそのまま渡し、Block.__checkMerkleRoot はヘッダーのフィールドとだけ比較します。
private __checkMerkleRoot(): boolean {
if (!this.transactions) throw errorMerkleNoTxes;
const actualMerkleRoot = Block.calculateMerkleRoot(this.transactions);
return tools.compare(this.merkleRoot!, actualMerkleRoot) === 0;
}
checkTxRoots() は witness commit のチェックを加えてから __checkMerkleRoot() を呼びます。そのため、本当のルートをコミットしたヘッダーは、詰め物に吸収された最後のトランザクションの余分なコピーを一覧が持っていても通過します。攻撃者はマイニングする必要がありません。ヘッダー、プルーフ・オブ・ワーク、ブロック ID は同じままです。
ブロック ID は 80 バイトのヘッダーだけをハッシュし、ヘッダーはマークルルートを通じてのみトランザクション一覧を認証します。したがって、ルートを保つ一覧の変更は ID と、それに基づくすべての検索を保ちます。
同じ複製の種類が CVE-2012-2459 で、Bitcoin Core が 2012 年に修正した改変です。アドバイザリは Core が ComputeMerkleRoot と CheckBlock でこのケースを拒否すると述べています。これはライブラリの同等性の欠落であり、Bitcoin の新しい構築上の欠陥ではありません。
どのように悪用されますか?
攻撃は既存のブロックを取り、ヘッダーを変えずにトランザクション一覧を変更します。
- 攻撃者は、有効なヘッダーとプルーフ・オブ・ワークを持ち、マークル木のどこかのレベルでノード数が奇数になる実在のブロックを選びます。
- 攻撃者はブロックを解析し、最後のトランザクションのコピーを一覧に追加して再シリアライズします。アドバイザリのメインネットの例では 259 バイト増え、ヘッダーは変わりません。
- 攻撃者は改変したブロックを、bitcoinjs-lib でブロックを解析し
checkTxRoots()に依存するサービスへ中継します。 - サービスはマークルルートを再計算します。複製されたトランザクションが奇数レベルの欠落ノードを埋めるため、ルートとブロック ID はヘッダーと一致し、
checkTxRoots()は true を返します。 - サービスは、同じ ID を持つ本物のブロックとは異なるトランザクション一覧のブロックを見ます。索引付けサービスは複製されたトランザクションを二重に付与し、入金システムは二重に数える可能性があります。
アドバイザリは、実際のブロックの多くがどこかのレベルで奇数ノード数に達するため、既存のブロックの多くを書き換えられると述べています。
実証コードは、リポジトリの Bitcoin Core フィクスチャにある実在のメインネットブロックに対して実行されます。本物のブロックは 9 件のトランザクションで検証に通り、偽造コピーは 10 件、259 バイト増、同じブロック ID で、こちらも検証に通ります。アドバイザリは同じブロックを 12 件に拡張しても同じ結果になると述べています。
// 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
攻撃の流れは?
ピアが既存のブロックの最後のトランザクションを複製して書き換えます。ライブラリのルート計算は、複製を不可視にした詰め物を報告しないため、書き換えられた一覧を受け入れます。
flowchart LR
A["ピアが改変ブロックを中継"] --> B["Block.fromHex が複製トランザクションを解析"]
B --> C["fastMerkleRoot が奇数レベルを詰める"]
C --> D["複製ペアのチェックなし"]
D --> E["checkTxRoots が true を返す"]
E --> F["索引サービスが複製トランザクションを付与"]
攻撃者に何ができますか?
影響はライブラリではなく利用側にあります。偽造ブロックが受理されると、攻撃者は次を行えます。
checkTxRoots()をブロック整合性のゲートとして使うサービスに、同じトランザクションを二重に付与させる。- ブロック ID は一致するがチェーンと異なるトランザクション一覧を、エクスプローラーや索引サービスに保存させる。
- CVE-2012-2459 で説明されるサービス拒否を引き起こす。ブロック ID をブラックリスト化して反応する利用者は、その ID を持つ本物のブロックも拒否します。
攻撃には、信頼できないブロックを解析し checkTxRoots() に基づいて処理する利用者が必要です。秘密鍵を回復したり、トランザクションに署名したり、他人のコインを直接使ったりするものではありません。アドバイザリは、このライブラリの利用においてブロック検証はトランザクション処理より小さい部分であるため、重大度は利用者次第だと明言しています。
トランザクションの署名やブロードキャストだけを行うサービスは対象ではありません。影響を受けるのはブロックを読み取る側です。索引サービス、取引所、エクスプローラー、そしてライブラリがトランザクションルートの一致を報告したためにブロックを受け入れるすべての入金パイプラインが該当します。
修正が機能する理由
修正は詰め物のケースをルート計算の外へ報告し、実際の複製ペアが見つかったときにブロック検証を失敗させます。コミット b79c7714c0687473dc6e25c69fc5fc597302c7a0 は fastMerkleRoot に mutated 出力パラメータを追加し、__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);
}
重要なのは i + 1 < length の条件です。このレベルで本当に兄弟である 2 つのハッシュを比較し、奇数ノードの詰め物が作る合成ペアを除外します。単一の値は印を付けず、異なるペアにある等しいハッシュも印を付けません。これはコミットの新しいテストでも確認されています。
__checkMerkleRoot はフラグが立つと false を返します。
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
+ );
}
修正は 3 つの修正ブランチにあります。7.x はコミット b79c7714c0687473dc6e25c69fc5fc597302c7a0、6.x は 009f165edfbf925cad8964b2dce478d18dd19449、5.2.1 は f3434d1e4bb5e0784fe769c4cd08a243576232a1 で、外部依存の merkle-lib をローカル実装に置き換えています。修正はライブラリのブロックルート経路に対して完全です。各利用者のマークルコードにトランザクション重複チェックを追加するものではないため、アプリケーション側のチェックは引き続き有用です。
侵害されたかどうかを確認するには?
ここではホストの侵害を探すのではなく、利用側がどのブロックを受け入れるかが変わります。そのため証拠はブロックと入金の記録にあります。保存されたトランザクション一覧が別の情報源と一致しないブロックと、同じブロック内で繰り返されるトランザクション ID を探してください。
- 同じ高さとブロック ID について、保存されたブロックを別の Bitcoin ノードやエクスプローラーと比較します。
- トランザクション一覧に重複した
txidを含むブロックを探します。 - 古いライブラリで検証されたブロックに基づく付与や入金を監査します。
- 保存されたブロックに対して修正版で
checkTxRoots()を再実行し、新たに拒否されるブロックを記録します。
# 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
2 つ目の情報源との比較で失敗するブロックは調査すべき証拠です。一致するブロックは、利用側が正しく処理した証明にはなりません。不具合はチェーンではなく利用側の検証経路にあるためです。
年表
- 2026-09-07:修正が 5.2.1、6.1.8、7.0.2 で公開されます。
- 2026-10-08:CVE が予約されます。
- 2026-10-26:リポジトリが 6 件のアドバイザリを公開します。GHSA-8vv3-6qpc-gpw6 を含みます。
- 2026-11-04:CVE-2026-107846 の CVE レコードが公開されます。
よくある質問
この不具合でウォレットからビットコインを盗めますか?
単独では盗めません。鍵を露出せず、有効な署名も作りません。盗難リスクは間接的です。古いライブラリで検証したブロックから入金を付与するサービスは、同じトランザクションを二重に付与するよう誘導される可能性があります。
Bitcoin Core は偽造ブロックを受理しますか?
いいえ。アドバイザリは、Bitcoin Core が ComputeMerkleRoot で改変を検出し、bad-txns-duplicate でブロックを拒否すると述べています。この不具合は bitcoinjs-lib のブロック検証器と Core の動作の差です。
トランザクションを作成するだけでも更新が必要ですか?
この不具合については、コードがブロックも解析し checkTxRoots() を呼ぶ場合を除き不要です。トランザクションの構築と署名は影響するブロックルート経路を通りません。
偽造ブロックの ID が同じままなのはなぜですか?
ブロック ID はヘッダーのハッシュであり、ヘッダーは変わりません。複製されたトランザクションはトランザクション一覧とブロックのシリアライズサイズだけを変え、どちらもヘッダーのハッシュの外にあります。
更新できない場合の回避策はありますか?
トランザクション複製型にはあります。checkTxRoots() を信頼する前に、トランザクション ID を繰り返すブロックを拒否してください。チェックは「修正方法」に示しており、更新までの一時的なものとして扱うべきです。
Ostorlab で古い bitcoinjs-lib のコピーを特定する
Ostorlab のソフトウェア構成分析と SBOM インベントリは、リポジトリやコンテナイメージ内の bitcoinjs-lib などのライブラリのバージョンを記録します。これにより、5.2.1、6.1.8、7.0.2 より前のバージョンをまだ解決しているサービスを特定できます。
参考資料
- リポジトリのアドバイザリ 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(2026 年 11 月 9 日確認)
- CVE レコード CVE-2026-107846(2026-11-04):https://www.cve.org/CVERecord?id=CVE-2026-107846(2026 年 11 月 9 日確認)
- 修正コミット b79c7714c0687473dc6e25c69fc5fc597302c7a0(2026-09-03):https://github.com/bitcoinjs/bitcoinjs-lib/commit/b79c7714c0687473dc6e25c69fc5fc597302c7a0(2026 年 11 月 9 日確認)
- 6.x 系の修正コミット 009f165edfbf925cad8964b2dce478d18dd19449(2026-09-03):https://github.com/bitcoinjs/bitcoinjs-lib/commit/009f165edfbf925cad8964b2dce478d18dd19449(2026 年 11 月 9 日確認)
- 5.2.1 の修正コミット f3434d1e4bb5e0784fe769c4cd08a243576232a1(2026-09-07):https://github.com/bitcoinjs/bitcoinjs-lib/commit/f3434d1e4bb5e0784fe769c4cd08a243576232a1(2026 年 11 月 9 日確認)
- CVE-2012-2459、マークル木の改変クラス(2026 年 11 月 9 日確認):https://www.cve.org/CVERecord?id=CVE-2012-2459
- bitcoinjs-lib の npm ダウンロード数、2026 年 11 月 2 日から 8 日:https://api.npmjs.org/downloads/point/last-week/bitcoinjs-lib(2026 年 11 月 9 日確認)