Skip to content

Commit afd2b60

Browse files
authored
fix: bound proof node size and count in verify_proof (#33)
## Summary - Proof nodes were decoded without size or count limits, allowing memory/CPU exhaustion via malicious proofs - Added `MAX_PROOF_NODE_SIZE` (1024 bytes) and `MAX_PROOF_NODES` (65) with upfront/per-iteration checks - Added `ProofNodeTooLarge` and `TooManyProofNodes` error variants Addresses [SeismicSystems/internal#206](SeismicSystems/internal#206).
1 parent 28f5479 commit afd2b60

3 files changed

Lines changed: 111 additions & 1 deletion

File tree

src/proof/error.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,20 @@ pub enum ProofVerificationError {
2929
UnexpectedEmptyRoot,
3030
/// Proof contains trailing nodes after the expected end.
3131
TrailingProofNodes,
32+
/// Individual proof node exceeds the maximum allowed size.
33+
ProofNodeTooLarge {
34+
/// The size of the oversized node.
35+
got: usize,
36+
/// The maximum allowed size.
37+
max: usize,
38+
},
39+
/// The proof contains more nodes than allowed.
40+
TooManyProofNodes {
41+
/// The number of nodes in the proof.
42+
got: usize,
43+
/// The maximum allowed number of nodes.
44+
max: usize,
45+
},
3246
/// Error during RLP decoding of trie node.
3347
Rlp(alloy_rlp::Error),
3448
}
@@ -66,6 +80,12 @@ impl fmt::Display for ProofVerificationError {
6680
Self::TrailingProofNodes => {
6781
write!(f, "proof contains trailing nodes after the expected end")
6882
}
83+
Self::ProofNodeTooLarge { got, max } => {
84+
write!(f, "proof node size {got} exceeds maximum {max}")
85+
}
86+
Self::TooManyProofNodes { got, max } => {
87+
write!(f, "proof node count {got} exceeds maximum {max}")
88+
}
6989
Self::Rlp(error) => fmt::Display::fmt(error, f),
7090
}
7191
}

src/proof/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
use alloc::vec::Vec;
55

66
mod verify;
7-
pub use verify::verify_proof;
7+
pub use verify::{MAX_PROOF_NODE_SIZE, MAX_PROOF_NODES, verify_proof};
88

99
mod error;
1010
pub use error::ProofVerificationError;

src/proof/verify.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ use alloy_rlp::{Decodable, EMPTY_STRING_CODE};
1111
use core::ops::Deref;
1212
use nybbles::Nibbles;
1313

14+
/// Maximum allowed size (in bytes) of a single proof node.
15+
///
16+
/// A fully populated branch node has 17 children x 33 bytes each, approximately 561 bytes of
17+
/// payload, plus RLP overhead. We use 1024 bytes as a safe upper bound.
18+
pub const MAX_PROOF_NODE_SIZE: usize = 1024;
19+
20+
/// Maximum allowed number of proof nodes.
21+
///
22+
/// A valid Merkle-Patricia proof path is bounded by the key length. For Keccak256 keys,
23+
/// that is 64 nibbles, so at most 65 nodes (including the root).
24+
pub const MAX_PROOF_NODES: usize = 65;
25+
1426
/// Verify the proof for given key value pair against the provided state root.
1527
///
1628
/// The expected node value can be either [Some] if it's expected to be present
@@ -25,6 +37,16 @@ pub fn verify_proof<'a, I>(
2537
where
2638
I: IntoIterator<Item = &'a Bytes>,
2739
{
40+
let proof: Vec<&'a Bytes> = proof.into_iter().collect();
41+
42+
// Enforce maximum proof node count.
43+
if proof.len() > MAX_PROOF_NODES {
44+
return Err(ProofVerificationError::TooManyProofNodes {
45+
got: proof.len(),
46+
max: MAX_PROOF_NODES,
47+
});
48+
}
49+
2850
let mut proof = proof.into_iter().peekable();
2951

3052
// If the proof is empty or contains only an empty node, the expected value must be None.
@@ -55,6 +77,13 @@ where
5577
let mut last_decoded_node = Some(NodeDecodingResult::Node(RlpNode::word_rlp(&root)));
5678
let mut last_decoded_node_is_private = false;
5779
for node in proof {
80+
// Enforce maximum proof node size.
81+
if node.len() > MAX_PROOF_NODE_SIZE {
82+
return Err(ProofVerificationError::ProofNodeTooLarge {
83+
got: node.len(),
84+
max: MAX_PROOF_NODE_SIZE,
85+
});
86+
}
5887
// Check if the node that we just decoded (or root node, if we just started) matches
5988
// the expected node from the proof.
6089
if Some(RlpNode::from_rlp(node).as_slice()) != last_decoded_node.as_deref() {
@@ -910,4 +939,65 @@ mod tests {
910939
}
911940
});
912941
}
942+
943+
#[test]
944+
fn reject_oversized_proof_node() {
945+
let key = Nibbles::unpack(B256::repeat_byte(0x42));
946+
let root = B256::repeat_byte(0x01);
947+
948+
// Create a proof node that exceeds MAX_PROOF_NODE_SIZE.
949+
let oversized_node = Bytes::from(vec![0xaa; MAX_PROOF_NODE_SIZE + 1]);
950+
let proof = vec![oversized_node];
951+
952+
let result = verify_proof(root, key, Some(vec![0x42]), false, proof.iter());
953+
assert_eq!(
954+
result,
955+
Err(ProofVerificationError::ProofNodeTooLarge {
956+
got: MAX_PROOF_NODE_SIZE + 1,
957+
max: MAX_PROOF_NODE_SIZE,
958+
})
959+
);
960+
}
961+
962+
#[test]
963+
fn reject_too_many_proof_nodes() {
964+
let key = Nibbles::unpack(B256::repeat_byte(0x42));
965+
let root = B256::repeat_byte(0x01);
966+
967+
// Create a proof with more nodes than MAX_PROOF_NODES.
968+
// The nodes don't need to be valid RLP because the count check
969+
// happens before decoding.
970+
let dummy_node = Bytes::from(vec![0xc0]); // minimal RLP empty list
971+
let proof: Vec<Bytes> = (0..MAX_PROOF_NODES + 1).map(|_| dummy_node.clone()).collect();
972+
973+
let result = verify_proof(root, key, Some(vec![0x42]), false, proof.iter());
974+
assert_eq!(
975+
result,
976+
Err(ProofVerificationError::TooManyProofNodes {
977+
got: MAX_PROOF_NODES + 1,
978+
max: MAX_PROOF_NODES,
979+
})
980+
);
981+
}
982+
983+
#[test]
984+
fn accept_proof_at_max_node_size() {
985+
// A node exactly at MAX_PROOF_NODE_SIZE should NOT be rejected by the size check.
986+
// It will fail for other reasons (invalid RLP, root mismatch, etc.) but not size.
987+
let key = Nibbles::unpack(B256::repeat_byte(0x42));
988+
let root = B256::repeat_byte(0x01);
989+
990+
let node = Bytes::from(vec![0xaa; MAX_PROOF_NODE_SIZE]);
991+
let proof = vec![node];
992+
993+
let result = verify_proof(root, key, Some(vec![0x42]), false, proof.iter());
994+
// Should not be ProofNodeTooLarge - it may fail for other reasons
995+
assert_ne!(
996+
result,
997+
Err(ProofVerificationError::ProofNodeTooLarge {
998+
got: MAX_PROOF_NODE_SIZE,
999+
max: MAX_PROOF_NODE_SIZE,
1000+
})
1001+
);
1002+
}
9131003
}

0 commit comments

Comments
 (0)