Skip to content

Fix 1910 - #1921

Merged
oberstet merged 3 commits into
crossbario:masterfrom
oberstet:fix_1910
Jul 14, 2026
Merged

Fix 1910#1921
oberstet merged 3 commits into
crossbario:masterfrom
oberstet:fix_1910

Conversation

@oberstet

Copy link
Copy Markdown
Contributor

fixes #1910 - will be done with proper red-green two-phase TDD

oberstet added 3 commits July 14, 2026 19:14
…rossbario#1910)

Only permessage-deflate bounds decompressed output (its max_message_size cap,
fixed in crossbario#1908). snappy, bzip2 and brotli decompress a frame fully and
unbounded, so a compressed frame can inflate into memory before any size check
sees it (potential memory exhaustion; the security-critical bypass itself was
already closed at the protocol layer in crossbario#1909).

Introduce a backend-neutral test module (not USE_TWISTED-gated, so it runs
under both the Twisted trial and asyncio pytest coverage phases) that drives
the shared compression backends directly:

- BoundedDecompressMaxOutputLenTests: parametrized over deflate/bzip2/snappy/
  brotli (optional codecs subTest-skipped when absent) - an over-budget message
  passed with max_output_len must raise PayloadExceededError; an under-budget
  message round-trips byte-exact; the default (no max_output_len) stays
  unbounded.
- PerMessageDeflateMaxMessageSizeTests: the deflate max_message_size tests
  relocated from test_websocket_frame.py (which is Twisted-phase only) so they
  now also run under asyncio.

To make the red behavioural rather than a bare TypeError, max_output_len=None
is added to the four decompress_message_data() signatures but not yet honored,
so test_bounded_rejects_oversized fails with "PayloadExceededError not raised"
(the bound is ignored) - the enforcement lands in the next commit.

Note: This work was completed with AI assistance (Claude Code).
…bario#1910)

Bound decompressed output uniformly, so a compressed frame cannot inflate
unbounded into memory before the protocol-level size check (the security
bypass itself was already closed in crossbario#1909; this closes the peak-memory gap and
brings snappy/bzip2/brotli to parity with deflate).

- compress_base.py: document the bounded-decompress contract on the
  PerMessageCompress base class - decompress_message_data(data,
  max_output_len=None) returns at most max_output_len octets or raises
  PayloadExceededError, never truncates; None keeps the unbounded default.
- deflate: generalize the crossbario#1908 tail-drain to bound by the tighter of the
  extension-level max_message_size (cumulative) and the per-call max_output_len.
- bzip2: use BZ2Decompressor.decompress(data, max_length) capped one octet over
  budget and reject when the output exceeds it (needs_input is unreliable - it
  also goes False at end-of-stream). Native incremental cap.
- snappy, brotli: their libraries expose no output-length argument, so the
  frame is inflated in full (already bounded on the wire by maxFramePayloadSize)
  and then checked - a weaker but still clean per-frame guarantee, documented.
- protocol.py: at the inflation site pass the remaining maxMessagePayloadSize
  budget as max_output_len so deflate/bzip2 stop early, and translate a
  PayloadExceededError into the same MESSAGE_TOO_BIG failure. The crossbario#1909
  post-inflation check remains as the backstop for snappy/brotli.

Makes test_bounded_rejects_oversized (added in the previous commit) pass across
deflate/bzip2/brotli (snappy when installed), both backends, while under-budget
and exactly-at-budget messages round-trip byte-exact.

Note: This work was completed with AI assistance (Claude Code).
@oberstet
oberstet merged commit 583b534 into crossbario:master Jul 14, 2026
45 checks passed
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.

[BUG] Backend-agnostic bounded decompression for permessage-compress (snappy / bzip2 / brotli parity with deflate)

1 participant