Skip to content

Commit 8f8f2ce

Browse files
authored
Fix 2250 (#2255)
* start new dev branch; add audit file * Add failing tests: WebSocket compression config must be validated (#2250) check_websocket_compression() is an empty "# FIXME" stub, yet it is wired into check_websocket_options(), so WebSocket `compression` config is accepted entirely unvalidated: unknown codecs, unknown deflate attributes, wrong types and out-of-range window-bits / memory-level all pass silently and only surface (if at all) as a run-time error deep inside the compression backend. Add tests to CheckWebsocketTests covering what crossbar.router.protocol.set_websocket_options actually reads for deflate (request_no_context_takeover, request_max_window_bits, no_context_takeover, max_window_bits, memory_level), asserting malformed blocks are rejected and valid ones accepted. The permissible values mirror autobahn's PerMessageDeflateMixin: window bits 9..15 (plus 0 = "not requested" for request_max_window_bits) and memory level 1..9. Seven rejection tests fail ("InvalidConfigException not raised") because the validator is empty; the validator lands in the next commit. Note: This work was completed with AI assistance (Claude Code). * Implement the WebSocket compression config validator (#2250) Fill the empty check_websocket_compression() stub, mirroring the structure of check_websocket_options(): check_dict_args() for permitted keys and types, then explicit range checks. Only permessage-deflate is implemented in set_websocket_options(), so `deflate` is the only accepted codec, and its accepted attributes are exactly the ones set_websocket_options() reads. The permissible window-bits and memory-level values are imported from autobahn's PerMessageDeflateMixin (WINDOW_SIZE_PERMISSIBLE_VALUES / MEM_LEVEL_PERMISSIBLE_VALUES) rather than duplicated, so the validator cannot drift from what autobahn actually accepts at run-time. request_max_window_bits additionally accepts 0, matching autobahn's "do not request a window size" semantics. Makes the tests added in the previous commit pass. Note: This work was completed with AI assistance (Claude Code).
1 parent c4d9d42 commit 8f8f2ce

3 files changed

Lines changed: 126 additions & 1 deletion

File tree

.audit/oberstet_fix_2250.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-15
7+
Related issue(s): #2250
8+
Branch: oberstet:fix_2250

src/crossbar/common/checkconfig.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
identify_realm_name_category,
2828
)
2929
from autobahn.wamp.uri import convert_starred_uri
30+
from autobahn.websocket.compress_deflate import PerMessageDeflateMixin
3031
from autobahn.websocket.util import parse_url
3132
from yaml import Dumper, Loader, SafeDumper, SafeLoader
3233
from yaml.constructor import ConstructorError
@@ -1594,10 +1595,56 @@ def check_websocket_compression(options):
15941595
"""
15951596
Check options for WebSocket compression.
15961597
1598+
Only ``permessage-deflate`` is implemented (see
1599+
``crossbar.router.protocol.set_websocket_options``). The permissible values
1600+
for the deflate parameters are taken from the WebSocket compression
1601+
implementation in autobahn, so this validator cannot drift from what is
1602+
actually accepted at run-time.
1603+
15971604
http://crossbar.io/docs/
15981605
https://github.com/crossbario/crossbar/blob/master/docs/pages/administration/router/transport/WebSocket-Compression.md
1606+
1607+
:param options: The options to check.
1608+
:type options: dict
15991609
"""
1600-
# FIXME
1610+
check_dict_args({"deflate": (False, [Mapping])}, options, "WebSocket compression options")
1611+
1612+
if "deflate" in options:
1613+
deflate = options["deflate"]
1614+
1615+
check_dict_args(
1616+
{
1617+
"request_no_context_takeover": (False, [bool]),
1618+
"request_max_window_bits": (False, [int]),
1619+
"no_context_takeover": (False, [bool]),
1620+
"max_window_bits": (False, [int]),
1621+
"memory_level": (False, [int]),
1622+
},
1623+
deflate,
1624+
"WebSocket permessage-deflate compression options",
1625+
)
1626+
1627+
for k in ["max_window_bits", "request_max_window_bits"]:
1628+
if k in deflate:
1629+
val = deflate[k]
1630+
# 0 on request_max_window_bits means "do not request a window size"
1631+
if k == "request_max_window_bits" and val == 0:
1632+
continue
1633+
if val not in PerMessageDeflateMixin.WINDOW_SIZE_PERMISSIBLE_VALUES:
1634+
raise InvalidConfigException(
1635+
"invalid value {} for '{}' in WebSocket permessage-deflate options - permissible values {}".format(
1636+
val, k, PerMessageDeflateMixin.WINDOW_SIZE_PERMISSIBLE_VALUES
1637+
)
1638+
)
1639+
1640+
if "memory_level" in deflate:
1641+
val = deflate["memory_level"]
1642+
if val not in PerMessageDeflateMixin.MEM_LEVEL_PERMISSIBLE_VALUES:
1643+
raise InvalidConfigException(
1644+
"invalid value {} for 'memory_level' in WebSocket permessage-deflate options - permissible values {}".format(
1645+
val, PerMessageDeflateMixin.MEM_LEVEL_PERMISSIBLE_VALUES
1646+
)
1647+
)
16011648

16021649

16031650
def check_web_path_service_websocket_reverseproxy(personality, config):

src/crossbar/test/test_checkconfig.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,76 @@ def test_tiny_timeout_auto_ping(self):
216216

217217
self.assertTrue("'auto_ping_timeout' is in milliseconds" in str(ctx.exception))
218218

219+
#
220+
# WebSocket compression options (#2250). check_websocket_compression() was an
221+
# empty stub, so malformed compression config was silently accepted. The
222+
# permissible deflate values mirror autobahn's PerMessageDeflateMixin:
223+
# window bits 9..15 (plus 0 = "not requested" for request_max_window_bits)
224+
# and memory level 1..9.
225+
#
226+
227+
def test_compression_must_be_dict(self):
228+
with self.assertRaises(checkconfig.InvalidConfigException) as ctx:
229+
checkconfig.check_websocket_options({"compression": {"deflate": "nope"}})
230+
self.assertIn("deflate", str(ctx.exception))
231+
232+
def test_compression_unknown_codec(self):
233+
with self.assertRaises(checkconfig.InvalidConfigException) as ctx:
234+
checkconfig.check_websocket_options({"compression": {"bogus": {}}})
235+
self.assertIn("bogus", str(ctx.exception))
236+
237+
def test_compression_deflate_unknown_attribute(self):
238+
with self.assertRaises(checkconfig.InvalidConfigException) as ctx:
239+
checkconfig.check_websocket_options({"compression": {"deflate": {"bogus": 1}}})
240+
self.assertIn("bogus", str(ctx.exception))
241+
242+
def test_compression_deflate_bad_attribute_type(self):
243+
with self.assertRaises(checkconfig.InvalidConfigException) as ctx:
244+
checkconfig.check_websocket_options({"compression": {"deflate": {"no_context_takeover": "yes"}}})
245+
self.assertIn("no_context_takeover", str(ctx.exception))
246+
247+
def test_compression_deflate_bad_max_window_bits(self):
248+
# 8 is not a permissible deflate window size (9..15)
249+
with self.assertRaises(checkconfig.InvalidConfigException) as ctx:
250+
checkconfig.check_websocket_options({"compression": {"deflate": {"max_window_bits": 8}}})
251+
self.assertIn("max_window_bits", str(ctx.exception))
252+
253+
def test_compression_deflate_bad_request_max_window_bits(self):
254+
# 16 is out of range; 0 ("not requested") and 9..15 are the valid values
255+
with self.assertRaises(checkconfig.InvalidConfigException) as ctx:
256+
checkconfig.check_websocket_options({"compression": {"deflate": {"request_max_window_bits": 16}}})
257+
self.assertIn("request_max_window_bits", str(ctx.exception))
258+
259+
def test_compression_deflate_bad_memory_level(self):
260+
# 10 is out of range (1..9)
261+
with self.assertRaises(checkconfig.InvalidConfigException) as ctx:
262+
checkconfig.check_websocket_options({"compression": {"deflate": {"memory_level": 10}}})
263+
self.assertIn("memory_level", str(ctx.exception))
264+
265+
def test_compression_deflate_valid(self):
266+
# a fully-specified, valid deflate block is accepted
267+
checkconfig.check_websocket_options(
268+
{
269+
"compression": {
270+
"deflate": {
271+
"request_no_context_takeover": True,
272+
"request_max_window_bits": 11,
273+
"no_context_takeover": False,
274+
"max_window_bits": 15,
275+
"memory_level": 4,
276+
}
277+
}
278+
}
279+
)
280+
281+
def test_compression_deflate_request_max_window_bits_zero(self):
282+
# 0 means "do not request a window size" and must be accepted
283+
checkconfig.check_websocket_options({"compression": {"deflate": {"request_max_window_bits": 0}}})
284+
285+
def test_compression_deflate_empty(self):
286+
# an empty deflate block (all defaults) is valid
287+
checkconfig.check_websocket_options({"compression": {"deflate": {}}})
288+
219289

220290
class CheckRealmTests(TestCase):
221291
"""

0 commit comments

Comments
 (0)