Skip to content

Commit 32f8be0

Browse files
committed
Fixed CVE-2026-44545: Limit WebSocket sizes in autobahn config.
Fixed a denial of service vulnerability via unbounded WebSocket message sizes. Daphne previously passed no message or frame size limits to autobahn, whose defaults are unbounded. This allowed an unauthenticated client to exhaust server memory by sending a very large WebSocket messages/frames (CVE-2026-44545). Both limits now default to 1 MiB and can be configured via the new ``--websocket-max-message-size`` and ``--websocket-max-frame-size`` CLI flags (or the matching ``Server`` constructor arguments). Pass ``0`` to restore the previous unlimited behaviour. Thanks to ParkHyunWoo for the report.
1 parent 2628b7b commit 32f8be0

5 files changed

Lines changed: 121 additions & 2 deletions

File tree

CHANGELOG.txt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
1-
4.2.2 (UNRELEASED)
1+
4.2.2 (2026-06-03)
22
------------------
33

4+
* Fixed a denial of service vulnerability via unbounded WebSocket message sizes.
5+
Daphne previously passed no message or frame size limits to autobahn,
6+
whose defaults are unbounded. This allowed an unauthenticated client
7+
to exhaust server memory by sending a very large WebSocket
8+
messages/frames (CVE-2026-44545).
9+
10+
Both limits now default to 1 MiB and can be configured via the new
11+
``--websocket-max-message-size`` and ``--websocket-max-frame-size`` CLI
12+
flags (or the matching ``Server`` constructor arguments). Pass ``0`` to
13+
restore the previous unlimited behaviour.
14+
15+
Thanks to ParkHyunWoo for the report.
16+
417
* Fixed a header injection vulnerability on the WebSocket upgrade path
518
(CVE-2026-44546).
619

daphne/cli.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,22 @@ def __init__(self):
107107
help="The number of seconds before a WebSocket is closed if no response to a keepalive ping",
108108
default=30,
109109
)
110+
self.parser.add_argument(
111+
"--websocket-max-message-size",
112+
type=int,
113+
help="Maximum size, in bytes, of an incoming WebSocket message. "
114+
"0 disables the limit (not recommended; allows unauthenticated "
115+
"memory exhaustion).",
116+
default=1024 * 1024,
117+
)
118+
self.parser.add_argument(
119+
"--websocket-max-frame-size",
120+
type=int,
121+
help="Maximum size, in bytes, of a single incoming WebSocket frame. "
122+
"0 disables the limit (not recommended; allows unauthenticated "
123+
"memory exhaustion).",
124+
default=1024 * 1024,
125+
)
110126
self.parser.add_argument(
111127
"--application-close-timeout",
112128
type=int,
@@ -275,6 +291,8 @@ def run(self, args):
275291
websocket_timeout=args.websocket_timeout,
276292
websocket_connect_timeout=args.websocket_connect_timeout,
277293
websocket_handshake_timeout=args.websocket_connect_timeout,
294+
websocket_max_message_size=args.websocket_max_message_size,
295+
websocket_max_frame_size=args.websocket_max_frame_size,
278296
application_close_timeout=args.application_close_timeout,
279297
action_logger=(
280298
AccessLogGenerator(access_log_stream) if access_log_stream else None

daphne/server.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ def __init__(
6363
proxy_forwarded_proto_header=None,
6464
verbosity=1,
6565
websocket_handshake_timeout=5,
66+
websocket_max_message_size=1024 * 1024,
67+
websocket_max_frame_size=1024 * 1024,
6668
application_close_timeout=10,
6769
ready_callable=None,
6870
server_name="daphne",
@@ -83,6 +85,8 @@ def __init__(
8385
self.websocket_timeout = websocket_timeout
8486
self.websocket_connect_timeout = websocket_connect_timeout
8587
self.websocket_handshake_timeout = websocket_handshake_timeout
88+
self.websocket_max_message_size = websocket_max_message_size
89+
self.websocket_max_frame_size = websocket_max_frame_size
8690
self.application_close_timeout = application_close_timeout
8791
self.root_path = root_path
8892
self.verbosity = verbosity
@@ -104,6 +108,8 @@ def run(self):
104108
autoPingTimeout=self.ping_timeout,
105109
allowNullOrigin=True,
106110
openHandshakeTimeout=self.websocket_handshake_timeout,
111+
maxMessagePayloadSize=self.websocket_max_message_size,
112+
maxFramePayloadSize=self.websocket_max_frame_size,
107113
)
108114
if self.verbosity <= 1:
109115
# Redirect the Twisted log to nowhere

daphne/testing.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,21 @@ class BaseDaphneTestingInstance:
1818
startup_timeout = 2
1919

2020
def __init__(
21-
self, xff=False, http_timeout=None, request_buffer_size=None, *, application
21+
self,
22+
xff=False,
23+
http_timeout=None,
24+
request_buffer_size=None,
25+
websocket_max_message_size=None,
26+
websocket_max_frame_size=None,
27+
*,
28+
application,
2229
):
2330
self.xff = xff
2431
self.http_timeout = http_timeout
2532
self.host = "127.0.0.1"
2633
self.request_buffer_size = request_buffer_size
34+
self.websocket_max_message_size = websocket_max_message_size
35+
self.websocket_max_frame_size = websocket_max_frame_size
2736
self.application = application
2837

2938
def get_application(self):
@@ -41,6 +50,10 @@ def __enter__(self):
4150
kwargs["proxy_forwarded_proto_header"] = "X-Forwarded-Proto"
4251
if self.http_timeout:
4352
kwargs["http_timeout"] = self.http_timeout
53+
if self.websocket_max_message_size is not None:
54+
kwargs["websocket_max_message_size"] = self.websocket_max_message_size
55+
if self.websocket_max_frame_size is not None:
56+
kwargs["websocket_max_frame_size"] = self.websocket_max_frame_size
4457
# Start up process
4558
self.process = DaphneProcess(
4659
host=self.host,

tests/test_websocket.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,75 @@ def test_binary_frames(self):
267267
"bytes": b"what is here? \xe2",
268268
}
269269

270+
def assert_oversized_frame_rejected(self, test_app):
271+
"""
272+
Sends a 16-byte text frame and asserts the application sees only
273+
connect + disconnect — i.e. autobahn dropped the connection (its
274+
default failByDrop behaviour) before dispatching the payload.
275+
"""
276+
test_app.add_send_messages([{"type": "websocket.accept"}])
277+
sock, _ = self.websocket_handshake(test_app)
278+
_, messages = test_app.get_received()
279+
self.assert_valid_websocket_connect_message(messages[0])
280+
self.websocket_send_frame(sock, "x" * 16)
281+
deadline = time.time() + 2
282+
final_messages = []
283+
while time.time() < deadline:
284+
_, final_messages = test_app.get_received()
285+
if any(m["type"] == "websocket.disconnect" for m in final_messages):
286+
break
287+
time.sleep(0.05)
288+
try:
289+
sock.close()
290+
except OSError:
291+
pass
292+
types = [m["type"] for m in final_messages]
293+
self.assertEqual(
294+
types,
295+
["websocket.connect", "websocket.disconnect"],
296+
"Oversized frame should not have been delivered to the "
297+
f"application, but got: {types}",
298+
)
299+
300+
def test_websocket_max_message_size(self):
301+
"""
302+
Tests that an incoming WebSocket message exceeding
303+
``websocket_max_message_size`` is rejected by autobahn before it
304+
reaches the application.
305+
"""
306+
# 16-byte frame > 8-byte message limit.
307+
with DaphneTestingInstance(websocket_max_message_size=8) as test_app:
308+
self.assert_oversized_frame_rejected(test_app)
309+
310+
def test_websocket_max_frame_size(self):
311+
"""
312+
Tests that an incoming WebSocket frame exceeding
313+
``websocket_max_frame_size`` is rejected by autobahn before it
314+
reaches the application, independently of the message size limit.
315+
"""
316+
# Large message limit, so the frame size limit is what trips.
317+
with DaphneTestingInstance(
318+
websocket_max_frame_size=8,
319+
websocket_max_message_size=1024 * 1024,
320+
) as test_app:
321+
self.assert_oversized_frame_rejected(test_app)
322+
323+
def test_websocket_max_message_size_allows_under_limit(self):
324+
"""
325+
Tests that messages under ``websocket_max_message_size`` are
326+
delivered to the application unchanged.
327+
"""
328+
with DaphneTestingInstance(websocket_max_message_size=64) as test_app:
329+
test_app.add_send_messages([{"type": "websocket.accept"}])
330+
sock, _ = self.websocket_handshake(test_app)
331+
_, messages = test_app.get_received()
332+
self.assert_valid_websocket_connect_message(messages[0])
333+
test_app.add_send_messages(
334+
[{"type": "websocket.send", "text": "ack"}]
335+
)
336+
self.websocket_send_frame(sock, "x" * 16)
337+
assert self.websocket_receive_frame(sock) == "ack"
338+
270339
def test_http_timeout(self):
271340
"""
272341
Tests that the HTTP timeout doesn't kick in for WebSockets

0 commit comments

Comments
 (0)