Skip to content

[BUG] WebSocket maxMessagePayloadSize is enforced against compressed size, bypassed after permessage-deflate inflation #1909

Description

@oberstet

Problem (GHSA-hxp9-w8x3-p566, reported by Team Atlanta)

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).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions