bitcoinjs-lib 接受伪造区块 (CVE-2026-107846)
bitcoinjs-lib 7.0.1 及更早版本会在真实区块 ID 下接受伪造的交易列表;已在 5.2.1、6.1.8 和 7.0.2 中修复。
TL;DR
- bitcoinjs-lib 会验证一个交易列表与真实区块不同的伪造区块。对等节点复制最后一笔交易来填补 Merkle 树奇数层缺失的节点,而
checkTxRoots()仍然返回真。 - 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 严重,而仓库公告评为高。公告中包含公开的概念验证;没有已知利用,截至 2026-11-09 不在 CISA KEV 中。
- 升级到 5.2.1、6.1.8 或 7.0.2。如果验证不可信区块,还要拒绝任何重复交易 ID 的区块。
我受影响吗?
以下条件必须全部成立:
- 使用
bitcoinjs-lib2.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 还发布了另外五个公告:PSBT 手续费低估、两个解析拒绝服务问题、接受无效紧凑目标和冲突 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。三者均于 2026-09-07 发布,比公告发布早七周。
- 对于 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
}
该检查覆盖重复交易形式的攻击。它不会把库变成完整的区块验证器,也不能替代升级。
根本原因是什么?
Merkle 根例程通过将最后一个节点与自身哈希来填充奇数层,并返回根而不报告进行了填充。比特币的 Merkle 树构造也做同样的填充。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() 添加见证提交检查,然后调用 __checkMerkleRoot()。因此,提交真实根的头部会通过,即使交易列表携带了被填充吸收的最后一笔交易的额外副本。攻击者不需要挖矿:头部、工作量证明和区块 ID 都保持不变。
区块 ID 只对 80 字节头部做哈希,而头部仅通过 Merkle 根认证交易列表。因此,任何保留根的列表更改都会保留 ID 以及所有以此为键的查找。
同一类重复问题是 CVE-2012-2459,即 Bitcoin Core 在 2012 年修复的变异。公告指出 Core 通过 ComputeMerkleRoot 和 CheckBlock 拒绝该情况。这是库中的一致性缺口,不是比特币的新构造缺陷。
如何被利用?
攻击取一个现有区块,在不改变其头部的情况下更改其交易列表:
- 攻击者选择一个链上真实区块,具有有效头部和工作量证明,其 Merkle 树在某一层具有奇数个节点。
- 攻击者解析该区块,将最后一笔交易的副本追加到列表并重新序列化。在公告的主网示例中这会增加 259 字节,并保持头部不变。
- 攻击者将变异区块转发给使用 bitcoinjs-lib 解析区块并以
checkTxRoots()为门槛的服务。 - 服务重新计算 Merkle 根。重复的交易填补了奇数层缺失的节点,因此根和区块 ID 与头部匹配,
checkTxRoots()返回真。 - 服务现在看到的是一个交易列表不同、但 ID 也属于诚实区块的区块。索引器可能重复记入该交易,存款系统可能重复计数。
公告指出,大多数真实区块在某一层会达到奇数节点数,因此大多数现有区块都可以被重写。
其概念验证针对仓库 Bitcoin Core 测试夹具中的一个真实主网区块运行。诚实区块有九笔交易并通过验证;伪造副本有十笔交易、多 259 字节、区块 ID 相同,也通过验证。公告还将同一区块扩展到十二笔交易,结果相同。
// 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 返回真"]
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 条件是关键部分。它比较该层中真正互为兄弟的两个哈希,并排除由奇数节点填充创建的合成对。单个值不会被标记,位于不同对中的两个相等哈希也不会被标记,提交中的新测试覆盖了这些情况。
当标志被设置时,__checkMerkleRoot 返回假:
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
+ );
}
修复存在于三个修复分支中:7.x 的提交 b79c7714c0687473dc6e25c69fc5fc597302c7a0、6.x 的 009f165edfbf925cad8964b2dce478d18dd19449,以及 5.2.1 的 f3434d1e4bb5e0784fe769c4cd08a243576232a1,后者还用本地实现替换了外部 merkle-lib 依赖。修复对库的区块根路径是完整的。它不会为每个调用者自己的 Merkle 代码添加重复交易检查,因此应用层检查仍然有用。
如何检查是否已遭入侵?
这里没有主机入侵可查:攻击改变的是消费者接受哪些区块,因此证据在区块和存款记录中。查找存储的交易列表与第二来源不一致的区块,以及同一区块内重复的交易 ID。
- 对同一高度和区块 ID,将存储的区块与第二个比特币节点或浏览器比较。
- 查找交易列表包含重复
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
与第二来源比较失败的区块是需要调查的证据。匹配的区块不能证明消费者正确处理了它,因为缺陷在消费者的验证路径中,而不在链上。
时间线
- 2026-09-07:修复在 5.2.1、6.1.8 和 7.0.2 中发布。
- 2026-10-08:CVE 被预留。
- 2026-10-26:仓库发布六个公告,包括 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,Merkle 树变异类(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 日检查)