nimiq-primitives: BlockInclusionProof interlink issue when hops are empty
Description
Impact
A logic flaw in BlockInclusionProof::is_block_proven causes the function to return true without performing any cryptographic verification when get_interlink_hops yields an empty hop list. This occurs when the target block is at the election block position immediately preceding the election head's epoch. An attacker providing transaction inclusion proofs can forge a MacroBlock header for that epoch position and have it accepted as "proven" without any hash or signature verification.
Patches
The patch for this vulnerability is formally released as part of v1.4.0.
Workarounds
No Workarounds
Resources
See PR.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A logic flaw in nimiq-primitives BlockInclusionProof skips verification when the hop list is empty, allowing forged macro block headers at a specific epoch position.
Root
Cause A logic flaw in the BlockInclusionProof::is_block_proven function within the nimiq-primitives crate causes the method to return true without performing any cryptographic verification when get_interlink_hops returns an empty hop list. This occurs specifically when the target block is at the election block position immediately preceding the election head's epoch [1][3]. The absence of hash or signature checks at that edge case bypasses the intended proof of inclusion.
Attack
Vector An attacker can supply a transaction inclusion proof that includes a forged MacroBlock header for that specific epoch position. No authentication or prior access is required beyond the ability to submit a proof to a node that processes BlockInclusionProof requests [1][4]. The empty hop list triggers the false return, so the forged header is accepted without any verification.
Impact
If successfully exploited, the attacker can convince the node that a non-existent or manipulated MacroBlock is part of the chain's proven history. This could lead to incorrect chain state, enabling double-spends or other transaction-level fraud depending on how the proof is consumed by higher-level logic [3].
Mitigation
The vulnerability is fixed in PR #3705, included in release v1.4.0 of the nimiq/core-rs-albatross repository [1][2]. Users must upgrade to that version or later. No workarounds are available [3].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2<= 0.2.0+ 1 more
- (no CPE)range: <= 0.2.0
- (no CPE)range: <1.4.0
Patches
1cc5a1d54bbbfFix for interlink in block inclusion proof
2 files changed · +26 −2
primitives/block/src/block_proof.rs+13 −2 modified@@ -79,9 +79,9 @@ impl BlockInclusionProof { election_head.block_number(), ); - // The election head might already have an interlink or prev_election_head link to the target + // An empty proof is only valid if the election head already references the target directly. if hops.is_empty() { - return true; + return Self::references_block(election_head, target); } // Check that the proof contains all needed hops @@ -129,4 +129,15 @@ impl BlockInclusionProof { } true } + + fn references_block(block: &MacroBlock, target: &MacroBlock) -> bool { + let target_hash = target.hash(); + + block.header.parent_election_hash == target_hash + || block + .header + .interlink + .as_ref() + .is_some_and(|interlink| interlink.contains(&target_hash)) + } }
primitives/block/tests/block_proof.rs+13 −0 modified@@ -1,6 +1,7 @@ use std::collections::HashMap; use nimiq_block::{BlockInclusionProof, MacroBlock, MacroHeader}; +use nimiq_hash::Hash; use nimiq_primitives::policy::Policy; #[test] @@ -85,6 +86,18 @@ fn test_is_block_proven() { let block_proof = BlockInclusionProof { proof: vec![] }; assert!(block_proof.is_block_proven(&blocks[&10], &blocks[&9])); + // Current election head: 10, target: forged 9. Claimed proof [] + let mut forged_parent_target = blocks[&9].clone(); + forged_parent_target.header.history_root = "forged-parent-target".hash(); + let block_proof = BlockInclusionProof { proof: vec![] }; + assert!(!block_proof.is_block_proven(&blocks[&10], &forged_parent_target)); + + // Current election head: 10, target: forged 8. Claimed proof [] + let mut forged_interlink_target = blocks[&8].clone(); + forged_interlink_target.header.history_root = "forged-interlink-target".hash(); + let block_proof = BlockInclusionProof { proof: vec![] }; + assert!(!block_proof.is_block_proven(&blocks[&10], &forged_interlink_target)); + // Current election head: 22, target: 1. Claimed proof [17 (wrong), 8, 4, 2] let block_proof = BlockInclusionProof { proof: vec![
Vulnerability mechanics
Root cause
"Missing cryptographic verification when `get_interlink_hops` returns an empty hop list causes `BlockInclusionProof::is_block_proven` to accept any target block as proven without checking that the election head actually references it."
Attack vector
An attacker submits a transaction inclusion proof where the target block is at the election block position immediately preceding the election head's epoch. In this scenario, `get_interlink_hops` returns an empty hop list, and the original code returned `true` without any verification [patch_id=1261433]. The attacker can forge a MacroBlock header for that epoch position—modifying arbitrary fields such as `history_root`—and the proof will be accepted as valid. No cryptographic hash or signature check is performed on the forged block. The attack requires no special privileges beyond the ability to submit a proof to a system that calls `is_block_proven`.
Affected code
The vulnerability is in `BlockInclusionProof::is_block_proven` in `primitives/block/src/block_proof.rs` [patch_id=1261433]. When `get_interlink_hops` returns an empty hop list, the original code unconditionally returned `true` without verifying that the election head actually references the target block. The patch adds a new helper function `references_block` and replaces the unconditional return with a call to it.
What the fix does
The patch replaces the unconditional `return true` with a call to the new helper `Self::references_block(election_head, target)` [patch_id=1261433]. This helper verifies that the election head either has a `parent_election_hash` equal to the target's hash, or that the target's hash appears in the election head's interlink. If neither condition holds, the proof is rejected. The added test cases confirm that forged blocks with empty proofs are now correctly rejected. This closes the logic gap by ensuring that even when no interlink hops are needed, the claimed target must actually be referenced by the election head.
Preconditions
- inputAttacker must be able to submit a BlockInclusionProof with an empty proof vector to a system that calls is_block_proven.
- networkThe target block position must be the election block immediately preceding the election head's epoch, causing get_interlink_hops to return an empty list.
Generated on May 21, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-799f-29jm-gr6cghsaADVISORY
- github.com/nimiq/core-rs-albatross/commit/cc5a1d54bbbffd1ea975bd2ee87d5f7b3b30bbf1ghsa
- github.com/nimiq/core-rs-albatross/pull/3705ghsa
- github.com/nimiq/core-rs-albatross/releases/tag/v1.4.0ghsa
- github.com/nimiq/core-rs-albatross/security/advisories/GHSA-799f-29jm-gr6cghsa
News mentions
0No linked articles in our index yet.