Skip to content

fix(vote): cap zlib decompression of untrusted vote staples - #40

Open
schenkty wants to merge 3 commits into
mainfrom
cursor/fix-vote-staple-zlib-bomb-eb95
Open

schenkty wants to merge 3 commits into
mainfrom
cursor/fix-vote-staple-zlib-bomb-eb95

Conversation

@schenkty

@schenkty schenkty commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Summary

VoteStaple::verify decompresses attacker-supplied compressed staple bytes with miniz_oxide::inflate::decompress_to_vec_zlib, which has no output-size limit, and does so before any signature or content validation. Because staples are decoded from remote representative responses (keetanetwork-client decode_staple/decode_staples, history pages) — and the representative is not fully trusted — a malicious or compromised peer can return a small compressed payload that inflates to an enormous buffer, exhausting client/process memory (a classic zlib decompression bomb).

Measured amplification with the pinned miniz_oxide 0.8.9: a ~200 KiB compressed input inflates to ~200 MiB (~1029x), scaling linearly (≈10 MB → ≈10 GB). No client-side response-body cap limits the compressed input.

This is the same class of issue reported as HIGH in the sibling TypeScript node (unbounded zlib decompression of untrusted input), verified independently here.

Fix

Decompress under a fixed output cap using decompress_to_vec_zlib_with_limit, mapping overflow to the existing VoteError::MalformedStaple. One-line behavior change to inflate, no public API change.

Why the 8 MiB cap is safe (does not reject a legitimate staple)

A staple's canonical form is SEQUENCE { blocks SEQUENCE OF OCTET STRING, votes SEQUENCE OF OCTET STRING }. It endorses one confirmed block set and carries at most one vote per representative (votes are de-duplicated by issuer in validate_vote_invariants). Blocks are small — their text fields are individually length-capped (e.g. the 1024-byte external field in keetanetwork-block validation) and a block serializes to a few KB; a vote certificate is an X.509-shaped record of similar order. So even a large round (hundreds of blocks and hundreds of representative votes at a few KB each) stays in the low single-digit megabytes.

8 MiB therefore leaves comfortable headroom above any realistic staple while bounding attacker-forced allocation by ~3 orders of magnitude versus the previously-unbounded path. The repository does not define a hard protocol maximum staple size; the cap is left at 8 MiB (per review, unchanged absent evidence it's wrong). If a precise protocol maximum is later established, tighten the constant to it. The rationale is captured in a doc comment on MAX_STAPLE_UNCOMPRESSED_BYTES.

Tests

cargo test -p keetanetwork-vote — all pass, including:

  • test_inflate_rejects_decompression_bomb — a tiny compressed input that would exceed the cap is rejected as MalformedStaple rather than allocated.
  • test_inflate_accepts_within_cap — normal payloads still round-trip.

Trace

  • Sink: keetanetwork-vote/src/staple.rs inflate (was decompress_to_vec_zlib, no cap)
  • Reached via VoteStaple::verify (inflates before verification) ← keetanetwork-client/src/codec.rs decode_staple ← remote representative responses.

Severity: medium (client/process memory-exhaustion DoS). The running node server that also parses staples is in a separate repository and would warrant the same fix there.

Draft — audit fix; do not merge without maintainer review.

Open in Web Open in Cursor 

VoteStaple::verify inflated attacker-supplied compressed staple bytes with
miniz_oxide::decompress_to_vec_zlib, which has no output-size limit, before any
signature or content validation. A malicious representative could return a small
compressed payload that inflates to an enormous buffer, exhausting client/process
memory (a zlib decompression bomb; measured ~1029x amplification).

Use decompress_to_vec_zlib_with_limit with a fixed cap sized to the largest
legitimate staple bundle, mapping overflow to VoteError::MalformedStaple.

Co-authored-by: Ty Schenk <schenkty@users.noreply.github.com>

@larseidsvoll larseidsvoll left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DoS class addressed: inflate uses decompress_to_vec_zlib_with_limit (8 MiB) and maps overflow to MalformedStaple. Confirm 8 MiB against largest legitimate staple (or document why) before Ready. Draft + Ty lock — no merge.

Document why the MAX_STAPLE_UNCOMPRESSED_BYTES cap is safe: a staple carries one
confirmed block set plus at most one vote per representative (votes de-duplicated
by issuer), blocks/votes are individually small (block text fields are
length-capped), so a realistic staple is low single-digit MB. 8 MiB leaves
headroom above any legitimate staple while bounding attacker-forced allocation.
No functional change; cap unchanged.

Co-authored-by: Ty Schenk <schenkty@users.noreply.github.com>

@larseidsvoll larseidsvoll left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cap rationale on MAX_STAPLE_UNCOMPRESSED_BYTES addresses prior COMMENT; inflate limit + bomb tests still look correct. Draft + Ty lock — no merge.

@larseidsvoll
larseidsvoll marked this pull request as ready for review September 16, 2026 00:50

@larseidsvoll larseidsvoll left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE (re-stamp after #45)

Tip moved for main merge (rustls / RUSTSEC-2026-0285). Diff vs prior APPROVE unchanged: 8 MiB zlib inflate cap + bomb tests.

Security Audit green on this tip. Lint green; Tests still running.

Review only — do not bot-merge. Humans merge. No @ humans.

@sonarqubecloud

Copy link
Copy Markdown

@schenkty
schenkty requested a review from sephynox September 16, 2026 01:35
@larseidsvoll

Copy link
Copy Markdown

No security issues found.

Tip 73fdb47b9ccfe6323cbbc3fae70f293651737203: checked zlib staple inflate via decompress_to_vec_zlib_with_limit (8 MiB cap → MalformedStaple).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants