Skip to content

Commit fe20525

Browse files
authored
Fix 2253 (#2267)
* start new dev branch; add audit file * Add failing tests: WebSocket max_frame_size must be range-validated (#2253) check_websocket_options() validates max_frame_size as an int only — nothing bounds it. That is a silent footgun: autobahn enforces frames with '0 < maxFramePayloadSize < length', so a negative value makes '0 < size' false and the check never fires. A config like {'max_frame_size': -1} passes 'crossbar check', looks like a tightened limit to an operator, and in fact means NO frame limit at all — the opposite of the intent. 0 is the documented 'unlimited' spelling; a negative value must not be a second, undocumented one. Add tests asserting max_frame_size rejects a negative value and an over-range (> 64MB) value, and accepts 0 (unlimited) and a sane positive value. RED as expected: the negative and over-range assertions fail (InvalidConfigException not raised); the 0 and positive cases already pass. Note: This work was completed with AI assistance (Claude Code). * Range-validate WebSocket max_frame_size [0, 64MB] (#2253) max_frame_size was type-checked only. Because autobahn enforces frames with '0 < maxFramePayloadSize < length', a negative value makes '0 < size' false and the limit never fires — so {'max_frame_size': -1} passed config validation and silently disabled the frame limit, the opposite of the operator's intent. - checkconfig.py: add check_transport_max_frame_size() validating an int in [0, 64MB] (0 = unlimited, the documented spelling; upper bound consistent with the WebSocket max_message_size policy), and wire it into check_websocket_options(). max_frame_size is WebSocket-only (RawSocket is frameless), so RawSocket is unaffected. - changelog: 26.7.1 entry. The compressed-vs-uncompressed semantics of both size knobs are already documented (WebSocket-Options 'Frame and Message Size Limits' + WebSocket Compression, added in #2250). Turns the max_frame_size tests green; full local gate: ruff + ty clean, test_checkconfig 44 passed, Sphinx dummy build clean. Note: This work was completed with AI assistance (Claude Code).
1 parent 46c782d commit fe20525

4 files changed

Lines changed: 59 additions & 0 deletions

File tree

.audit/oberstet_fix_2253.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): #2253
8+
Branch: oberstet:fix_2253

docs/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Changelog
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>`_)
1212
* **⚠ 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>`_)
13+
* fix: range-validate WebSocket ``max_frame_size`` (``[0, 64 MB]``, ``0`` = unlimited). It was type-checked only, so a negative value passed ``crossbar check`` and — because autobahn enforces frames with ``0 < maxFramePayloadSize < length`` — silently *disabled* the frame limit instead of tightening it (`#2253 <https://github.com/crossbario/crossbar/issues/2253>`_)
1314
* fix: validate RawSocket ``max_message_size`` over its actual protocol range ``[512, 16 MB]`` with a dedicated validator. It previously reused the WebSocket-shaped ``[1, 64 MB]`` check, so a RawSocket config with ``max_message_size`` below 512 or above 16 MB passed ``crossbar check`` but then raised ``AssertionError`` at transport start (autobahn's factory asserts the WAMP RawSocket handshake range ``2**(9+n)``, i.e. ``[512, 16 MB]``). Also corrects ``docs/RawSocket-Transport.rst``: the documented range (``1–64 MB``) and default (``128 kB``) were both wrong — the real default is 16 MB (`#2252 <https://github.com/crossbario/crossbar/issues/2252>`_)
1415
* 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>`_)
1516
* 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>`_)

src/crossbar/common/checkconfig.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,31 @@ def check_transport_max_message_size(max_message_size):
10311031
)
10321032

10331033

1034+
def check_transport_max_frame_size(max_frame_size):
1035+
"""
1036+
Check the ``max_frame_size`` parameter for a WebSocket transport.
1037+
1038+
This is the on-the-wire (compressed) per-frame limit; ``0`` means unlimited.
1039+
A negative value must be rejected: autobahn only enforces frames with
1040+
``0 < maxFramePayloadSize < length``, so a negative value silently disables
1041+
the limit instead of tightening it (crossbar #2253). Upper bound is kept
1042+
consistent with the WebSocket ``max_message_size`` policy (64 MB).
1043+
1044+
:param max_frame_size: The maximum frame size parameter to check.
1045+
:type max_frame_size: int
1046+
"""
1047+
if not isinstance(max_frame_size, int):
1048+
raise InvalidConfigException(
1049+
"'max_frame_size' attribute in transport must be int ({} encountered)".format(type(max_frame_size))
1050+
)
1051+
if max_frame_size < 0 or max_frame_size > 64 * 1024 * 1024:
1052+
raise InvalidConfigException(
1053+
"invalid value {} for 'max_frame_size' attribute in transport (must be from [0, 64MB], 0 = unlimited)".format(
1054+
max_frame_size
1055+
)
1056+
)
1057+
1058+
10341059
# WAMP RawSocket negotiates its receive limit as a length exponent 2**(9+n),
10351060
# n in [0, 15], so max_message_size must lie in [2**9, 2**24] = [512, 16 MB].
10361061
# autobahn's WampRawSocketFactory.setProtocolOptions asserts exactly this, so a
@@ -1622,6 +1647,9 @@ def check_websocket_options(options):
16221647
if "compression" in options:
16231648
check_websocket_compression(options["compression"])
16241649

1650+
if "max_frame_size" in options:
1651+
check_transport_max_frame_size(options["max_frame_size"])
1652+
16251653
if "max_message_size" in options:
16261654
check_transport_max_message_size(options["max_message_size"])
16271655

src/crossbar/test/test_checkconfig.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,28 @@ def test_compression_deflate_empty(self):
286286
# an empty deflate block (all defaults) is valid
287287
checkconfig.check_websocket_options({"compression": {"deflate": {}}})
288288

289+
#
290+
# max_frame_size range validation (#2253). It was type-checked only, so a
291+
# negative value was accepted — and autobahn only enforces frames with
292+
# ``0 < maxFramePayloadSize < length``, so a negative value silently
293+
# DISABLES the limit rather than tightening it.
294+
#
295+
296+
def test_max_frame_size_negative_rejected(self):
297+
with self.assertRaises(checkconfig.InvalidConfigException):
298+
checkconfig.check_websocket_options({"max_frame_size": -1})
299+
300+
def test_max_frame_size_over_range_rejected(self):
301+
with self.assertRaises(checkconfig.InvalidConfigException):
302+
checkconfig.check_websocket_options({"max_frame_size": 64 * 1024 * 1024 + 1})
303+
304+
def test_max_frame_size_zero_allowed(self):
305+
# 0 is the documented "unlimited" spelling
306+
checkconfig.check_websocket_options({"max_frame_size": 0})
307+
308+
def test_max_frame_size_positive_allowed(self):
309+
checkconfig.check_websocket_options({"max_frame_size": 65536})
310+
289311

290312
class CheckRealmTests(TestCase):
291313
"""

0 commit comments

Comments
 (0)