Skip to content

Commit d73ba82

Browse files
authored
Fix 2251 (#2260)
* start new dev branch; add audit file * Add failing tests: max_message_size must default to 16MB on both transports (#2251) Crossbar's two WAMP transports disagree on the default receive size limit, and the WebSocket default is no limit at all: - WebSocket (router/protocol.py:126 server, :151 client): maxMessagePayloadSize=c.get('max_message_size', 0) -> 0 == unlimited - RawSocket (router/protocol.py:562): c.get('max_message_size', None) -> None leaves autobahn's 16 MB (2**24) factory default in place. So the same knob means 'unlimited' on one transport and '16 MB' on the other, and a WebSocket transport that does not set it accepts messages of any size. This is the config the GHSA-hxp9-w8x3-p566 decompression-bomb fix is gated on: protection is only meaningful once max_message_size is actually set, so an unbounded default contradicts 'Secure by Default'. Add DefaultMaxMessageSizeTests asserting both transports default to 16 MB (and agree), plus a guard that an explicit value still wins. RED as expected: the three WebSocket-default assertions fail (0 != 16777216); the RawSocket default and the explicit-value guard already pass. Note: This work was completed with AI assistance (Claude Code). * Default max_message_size to 16MB on both transports (#2251) Secure by default: default max_message_size to 16 MB (2**24) on both WebSocket and RawSocket, so a transport that does not set the option is bounded rather than accepting messages of any size. - router/protocol.py: introduce DEFAULT_MAX_MESSAGE_SIZE = 2**24 and use it for the WebSocket server + client factory defaults (was 0 == unlimited) and for the RawSocket default (was None, which relied implicitly on autobahn's 16 MB factory default — now stated explicitly in one place). 16 MB is the largest a WAMP RawSocket can negotiate (handshake length exponent tops out at 2**(9+15)), hence the largest value common to both transports. - docs: update the WebSocket-Options table default and the size-limits prose, and add a prominent ⚠ behaviour-change entry to the 26.7.1 changelog (messages > 16 MB on a WebSocket transport with no explicit max_message_size are now rejected with close code 1009; remedy = set max_message_size explicitly). Turns DefaultMaxMessageSizeTests green (5/5). Full local gate: ruff + ty clean, router unit suite 100 passed / 13 skipped, Sphinx dummy build clean on the changed pages. Note: This work was completed with AI assistance (Claude Code).
1 parent a447065 commit d73ba82

5 files changed

Lines changed: 86 additions & 4 deletions

File tree

.audit/oberstet_fix_2251.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
- [ ] I did **not** use any AI-assistance tools to help create this pull request.
2+
- [x] I **did** use AI-assistance tools to *help* create this pull request.
3+
- [x] I have read, understood and followed the projects' [AI Policy](https://github.com/crossbario/autobahn-python/blob/main/AI_POLICY.md) when creating code, documentation etc. for this pull request.
4+
5+
Submitted by: @oberstet
6+
Date: 2026-07-17
7+
Related issue(s): #2251
8+
Branch: oberstet:fix_2251

docs/WebSocket-Options.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ The available options are:
6060
+---------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
6161
| max_frame_size | Maximum size in bytes of incoming WebSocket frames accepted or 0 to allow any size. (default: 0) |
6262
+---------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
63-
| max_message_size | Maximum size in bytes of incoming WebSocket messages accepted or 0 to allow any size. (default: 0) |
63+
| max_message_size | Maximum size in bytes of incoming WebSocket messages accepted, or 0 to allow any size. (default: 16777216 = 16 MB) |
6464
+---------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
6565
| auto_fragment_size | Automatically fragment outgoing WebSocket messages into WebSocket frames of payload maximum specified size in bytes or 0 to disable. (default: 0) |
6666
+---------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
@@ -111,6 +111,12 @@ decompression ("zip") bombs. Whenever you enable compression on a router-facing
111111
transport, set ``max_message_size`` to the largest message your application
112112
legitimately needs — see :doc:`WebSocket Compression <WebSocket-Compression>`.
113113

114+
Since Crossbar.io 26.7.1, ``max_message_size`` **defaults to 16 MB** (``2**24``)
115+
on both the WebSocket and RawSocket transports — a secure default (previously the
116+
WebSocket default was ``0``, i.e. unlimited). Set it to ``0`` to explicitly allow
117+
any size, or raise it (WebSocket validation permits up to 64 MB; RawSocket cannot
118+
exceed 16 MB by protocol).
119+
114120
Production Settings
115121
-------------------
116122

docs/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Changelog
99
------
1010

1111
* new: coordinated WAMP **26.7.1 release train** — pin ``autobahn>=26.7.1`` for the WebSocket permessage-deflate decompression-bomb fix (advisory ``GHSA-hxp9-w8x3-p566``) and relock ``uv.lock`` to the coordinated set (autobahn 26.7.1, zlmdb 26.7.1); ``cfxdb`` and ``txaio`` remain at 26.6.1 (`#2250 <https://github.com/crossbario/crossbar/issues/2250>`_)
12+
* **⚠ behaviour change** — ``max_message_size`` now **defaults to 16 MB** (``2**24``) on **both** the WebSocket and RawSocket transports (secure by default). Previously the WebSocket default was ``0`` (unlimited) while RawSocket already defaulted to 16 MB, so a WebSocket transport that did not set the option accepted messages of any size — leaving the router unbounded by default and the ``GHSA-hxp9-w8x3-p566`` protection effectively opt-in. Messages larger than 16 MB on a WebSocket transport with no explicit ``max_message_size`` are now rejected (close code 1009, ``MESSAGE_TOO_BIG``). Set ``max_message_size`` explicitly if you need more — ``0`` allows any size, and WebSocket permits up to 64 MB (RawSocket cannot exceed 16 MB by protocol) (`#2251 <https://github.com/crossbario/crossbar/issues/2251>`_)
1213
* fix: validate WebSocket ``compression`` transport configuration. ``check_websocket_compression()`` was an empty stub yet wired into ``check_websocket_options()``, so unknown codecs, unknown or mis-typed deflate attributes and out-of-range ``request_max_window_bits`` / ``max_window_bits`` / ``memory_level`` were all silently accepted and only surfaced (if at all) as a run-time error deep inside the compression backend (`#2250 <https://github.com/crossbario/crossbar/issues/2250>`_)
1314
* new: functional test asserting the router rejects a decompression ("zip") bomb — a payload tiny on the wire but inflating far past the transport's ``max_message_size`` — on its *uncompressed* reassembled size (close code 1009). Verified a genuine regression test: it fails against autobahn < 26.7.1 and passes against 26.7.1 (`#2250 <https://github.com/crossbario/crossbar/issues/2250>`_)
1415
* docs: clarify the ``max_frame_size`` (on-the-wire, per-frame) versus ``max_message_size`` (reassembled and decompressed, per-message) semantics on the WebSocket and RawSocket transports, and document ``max_message_size`` as the decompression-bomb guard in the WebSocket Compression docs (`#2250 <https://github.com/crossbario/crossbar/issues/2250>`_)

src/crossbar/router/protocol.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@
3434
"WampRawSocketClientProtocol",
3535
)
3636

37+
# Secure-by-default receive size limit (uncompressed / application level) for
38+
# BOTH WAMP transports (crossbar #2251). 16 MB (2**24) is the largest message a
39+
# WAMP RawSocket can negotiate at all — the handshake length exponent tops out
40+
# at 2**(9+15) — so it is the largest value common to WebSocket and RawSocket.
41+
# Operators can raise it explicitly on WebSocket (validation permits up to
42+
# 64 MB); RawSocket cannot exceed it by protocol.
43+
DEFAULT_MAX_MESSAGE_SIZE = 2**24
44+
3745

3846
def set_websocket_options(factory, options):
3947
"""
@@ -123,7 +131,7 @@ def accept(offers):
123131
requireMaskedClientFrames=c.get("require_masked_client_frames", True),
124132
applyMask=c.get("apply_mask", True),
125133
maxFramePayloadSize=c.get("max_frame_size", 0),
126-
maxMessagePayloadSize=c.get("max_message_size", 0),
134+
maxMessagePayloadSize=c.get("max_message_size", DEFAULT_MAX_MESSAGE_SIZE),
127135
autoFragmentSize=c.get("auto_fragment_size", 0),
128136
failByDrop=c.get("fail_by_drop", False),
129137
echoCloseCodeReason=c.get("echo_close_codereason", False),
@@ -148,7 +156,7 @@ def accept(offers):
148156
maskClientFrames=c.get("mask_client_frames", True),
149157
applyMask=c.get("apply_mask", True),
150158
maxFramePayloadSize=c.get("max_frame_size", 0),
151-
maxMessagePayloadSize=c.get("max_message_size", 0),
159+
maxMessagePayloadSize=c.get("max_message_size", DEFAULT_MAX_MESSAGE_SIZE),
152160
autoFragmentSize=c.get("auto_fragment_size", 0),
153161
failByDrop=c.get("fail_by_drop", False),
154162
echoCloseCodeReason=c.get("echo_close_codereason", False),
@@ -559,7 +567,7 @@ def set_rawsocket_options(factory, options):
559567
:type options: dict
560568
"""
561569
c = options
562-
factory.setProtocolOptions(maxMessagePayloadSize=c.get("max_message_size", None))
570+
factory.setProtocolOptions(maxMessagePayloadSize=c.get("max_message_size", DEFAULT_MAX_MESSAGE_SIZE))
563571

564572

565573
class WampRawSocketServerProtocol(rawsocket.WampRawSocketServerProtocol):
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#####################################################################################
2+
#
3+
# Copyright (c) typedef int GmbH
4+
# SPDX-License-Identifier: EUPL-1.2
5+
#
6+
#####################################################################################
7+
8+
import txaio
9+
10+
txaio.use_twisted() # noqa
11+
12+
from autobahn.twisted.rawsocket import WampRawSocketServerFactory
13+
from autobahn.twisted.websocket import WebSocketClientFactory, WebSocketServerFactory
14+
from twisted.trial import unittest
15+
16+
from crossbar.router.protocol import set_rawsocket_options, set_websocket_options
17+
18+
# 16 MB: the secure-by-default receive limit for both transports. It is the
19+
# largest value a WAMP RawSocket can negotiate (the handshake length exponent
20+
# tops out at 2**(9+15)), so it is the largest value common to both transports.
21+
DEFAULT_MAX_MESSAGE_SIZE = 2**24
22+
23+
24+
class DefaultMaxMessageSizeTests(unittest.TestCase):
25+
"""
26+
A transport that does not set ``max_message_size`` explicitly must default
27+
to 16 MB on BOTH WebSocket and RawSocket (crossbar #2251). Historically the
28+
WebSocket default was ``0`` (unlimited), disagreeing with RawSocket's 16 MB
29+
and leaving the router unbounded by default.
30+
"""
31+
32+
def test_websocket_server_default_is_16mb(self):
33+
factory = WebSocketServerFactory()
34+
set_websocket_options(factory, {})
35+
self.assertEqual(factory.maxMessagePayloadSize, DEFAULT_MAX_MESSAGE_SIZE)
36+
37+
def test_websocket_client_default_is_16mb(self):
38+
factory = WebSocketClientFactory()
39+
set_websocket_options(factory, {})
40+
self.assertEqual(factory.maxMessagePayloadSize, DEFAULT_MAX_MESSAGE_SIZE)
41+
42+
def test_rawsocket_server_default_is_16mb(self):
43+
factory = WampRawSocketServerFactory(lambda: None)
44+
set_rawsocket_options(factory, {})
45+
self.assertEqual(factory._max_message_size, DEFAULT_MAX_MESSAGE_SIZE)
46+
47+
def test_both_transports_agree_on_default(self):
48+
ws = WebSocketServerFactory()
49+
set_websocket_options(ws, {})
50+
rs = WampRawSocketServerFactory(lambda: None)
51+
set_rawsocket_options(rs, {})
52+
self.assertEqual(ws.maxMessagePayloadSize, rs._max_message_size)
53+
54+
def test_explicit_websocket_value_is_honored(self):
55+
# An explicit setting must still win over the new default (regression
56+
# guard: the default change must not clobber configured values).
57+
factory = WebSocketServerFactory()
58+
set_websocket_options(factory, {"max_message_size": 4096})
59+
self.assertEqual(factory.maxMessagePayloadSize, 4096)

0 commit comments

Comments
 (0)