On the WebSocket Hybi (v13) receive path, maxMessagePayloadSize is checked
against the declared, compressed frame length, before inflation, and the
inflated bytes are never re-counted. A client frame of ~22 compressed bytes can
inflate to 4096 bytes and reach onMessage even when the application configured
a 128-byte message limit.
Trace (src/autobahn/websocket/protocol.py):
onFrameBegin → _onMessageFrameBegin(self.current_frame.length) — the
compressed wire frame length (:1841).
onMessageFrameBegin → message_data_total_length += length and checks it
against maxMessagePayloadSize (:634, :636) — i.e. against compressed bytes.
onFrameData inflates at :1861; for v13, onMessageFrameData only appends
to frame_data (:667) and never adds the inflated length to
message_data_total_length. The reassembled message is joined (:690) and
delivered via _onMessage (:693) with no uncompressed-size ceiling.
This contradicts the documented contract ("re-assembled payloads",
docs/websocket/programming.rst:512), the send path (which checks
maxMessagePayloadSize against the uncompressed message, :2554), and the
RawSocket transport (which checks the uncompressed serialized message). Hixie
(v0) already accounts uncompressed bytes correctly (:657). Same boundary class
as CVE-2016-10544.
Severity
Config-gated denial-of-service (memory/CPU amplification, availability only; no
confidentiality/integrity impact). Requires two explicit opt-ins:
permessage-deflate enabled and maxMessagePayloadSize set (default 0 =
unlimited). Not exploitable in a default configuration.
Fix (approach)
Per META decision D1/D2: maxMessagePayloadSize bounds the uncompressed,
reassembled per-message size. Enforce it at the inflation site
(onFrameData), backend-agnostic and working in both message and streaming mode:
- Account the uncompressed length per frame (
uncompressedLen, already
computed at :1862) into the running message total, and check it against
maxMessagePayloadSize there — replacing reliance on the pre-inflation
compressed-length accounting for the message cap.
- For deflate, use the bounded decompress primitive from child 01 so a single
frame's inflation stops at the remaining budget (memory bounded during
inflation, not merely detected after). For codecs without a native output cap,
post-decompress accounting still enforces the cap; per-frame wire size stays
bounded by maxFramePayloadSize. (Full multi-codec bounded-during-inflation =
child 03.)
- Keep
maxFramePayloadSize semantics (wire/per-frame) unchanged.
- On exceed, use the existing
_max_message_size_exceeded / MESSAGE_TOO_BIG
path (:998), setting wasMaxMessagePayloadSizeExceeded.
- Preserve exact current behavior when
maxMessagePayloadSize == 0.
Care: must not double-count (compressed accounting at :634 vs new uncompressed
accounting), must keep Hixie (v0) path correct, and must not regress the
streaming/frame API.
Red/green test plan (TDD, on this PR)
- Workhorse unit test modeled on the advisory PoC: drive
_dataReceived
directly (no network, deterministic), configure maxMessagePayloadSize = N,
send a compressed frame that inflates to > N, assert the message is
rejected (connection failed with MESSAGE_TOO_BIG,
wasMaxMessagePayloadSizeExceeded true) and not delivered to _onMessage.
Prove RED in CI (today it is delivered), then GREEN after the fix.
- Matrix (parametrized):
- backends: Twisted and asyncio;
- codecs: deflate (primary), plus snappy/bzip2/brotli where available
(post-decompress enforcement must hold for all);
- modes: whole-message and streaming (frame-based) consumer;
- controls: uncompressed message just under N is delivered intact (no false
positive); multi-frame fragmented message enforced on cumulative uncompressed
total.
- RawSocket regression: a companion assertion that RawSocket continues to
enforce maxMessagePayloadSize on the (uncompressed) serialized message, so the
cross-transport contract is proven consistent.
Acceptance criteria
maxMessagePayloadSize bounds the uncompressed reassembled message on WebSocket
receive, in both backends and both processing modes, for every compression
backend.
- Advisory PoC no longer bypasses the limit; oversize → clean
MESSAGE_TOO_BIG.
- Under-limit traffic unaffected;
maxMessagePayloadSize == 0 behavior identical
to today.
- New parametrized test proven red-before / green-after in CI on this PR.
References
This work is being completed with AI assistance (Claude Code).
Problem (GHSA-hxp9-w8x3-p566, reported by Team Atlanta)
On the WebSocket Hybi (v13) receive path,
maxMessagePayloadSizeis checkedagainst the declared, compressed frame length, before inflation, and the
inflated bytes are never re-counted. A client frame of ~22 compressed bytes can
inflate to 4096 bytes and reach
onMessageeven when the application configureda 128-byte message limit.
Trace (
src/autobahn/websocket/protocol.py):onFrameBegin→_onMessageFrameBegin(self.current_frame.length)— thecompressed wire frame length (
:1841).onMessageFrameBegin→message_data_total_length += lengthand checks itagainst
maxMessagePayloadSize(:634,:636) — i.e. against compressed bytes.onFrameDatainflates at:1861; for v13,onMessageFrameDataonly appendsto
frame_data(:667) and never adds the inflated length tomessage_data_total_length. The reassembled message is joined (:690) anddelivered via
_onMessage(:693) with no uncompressed-size ceiling.This contradicts the documented contract ("re-assembled payloads",
docs/websocket/programming.rst:512), the send path (which checksmaxMessagePayloadSizeagainst the uncompressed message,:2554), and theRawSocket transport (which checks the uncompressed serialized message). Hixie
(v0) already accounts uncompressed bytes correctly (
:657). Same boundary classas CVE-2016-10544.
Severity
Config-gated denial-of-service (memory/CPU amplification, availability only; no
confidentiality/integrity impact). Requires two explicit opt-ins:
permessage-deflate enabled and
maxMessagePayloadSizeset (default0=unlimited). Not exploitable in a default configuration.
Fix (approach)
Per META decision D1/D2:
maxMessagePayloadSizebounds the uncompressed,reassembled per-message size. Enforce it at the inflation site
(
onFrameData), backend-agnostic and working in both message and streaming mode:uncompressedLen, alreadycomputed at
:1862) into the running message total, and check it againstmaxMessagePayloadSizethere — replacing reliance on the pre-inflationcompressed-length accounting for the message cap.
frame's inflation stops at the remaining budget (memory bounded during
inflation, not merely detected after). For codecs without a native output cap,
post-decompress accounting still enforces the cap; per-frame wire size stays
bounded by
maxFramePayloadSize. (Full multi-codec bounded-during-inflation =child 03.)
maxFramePayloadSizesemantics (wire/per-frame) unchanged._max_message_size_exceeded/MESSAGE_TOO_BIGpath (
:998), settingwasMaxMessagePayloadSizeExceeded.maxMessagePayloadSize == 0.Care: must not double-count (compressed accounting at
:634vs new uncompressedaccounting), must keep Hixie (v0) path correct, and must not regress the
streaming/frame API.
Red/green test plan (TDD, on this PR)
_dataReceiveddirectly (no network, deterministic), configure
maxMessagePayloadSize = N,send a compressed frame that inflates to
> N, assert the message isrejected (connection failed with
MESSAGE_TOO_BIG,wasMaxMessagePayloadSizeExceededtrue) and not delivered to_onMessage.Prove RED in CI (today it is delivered), then GREEN after the fix.
(post-decompress enforcement must hold for all);
positive); multi-frame fragmented message enforced on cumulative uncompressed
total.
enforce
maxMessagePayloadSizeon the (uncompressed) serialized message, so thecross-transport contract is proven consistent.
Acceptance criteria
maxMessagePayloadSizebounds the uncompressed reassembled message on WebSocketreceive, in both backends and both processing modes, for every compression
backend.
MESSAGE_TOO_BIG.maxMessagePayloadSize == 0behavior identicalto today.
References
This work is being completed with AI assistance (Claude Code).