Version in base suite: 4.1.2-2 Base version: python-daphne_4.1.2-2 Target version: python-daphne_4.1.2-2+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/p/python-daphne/python-daphne_4.1.2-2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/p/python-daphne/python-daphne_4.1.2-2+deb13u1.dsc changelog | 10 patches/0001-Fixed-CVE-2026-44546-Prevent-header-injection-on-Web.patch | 154 +++++++ patches/0002-Fixed-CVE-2026-44545-Limit-WebSocket-sizes-in-autoba.patch | 212 ++++++++++ patches/series | 2 4 files changed, 378 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmphmwljvch/python-daphne_4.1.2-2.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmphmwljvch/python-daphne_4.1.2-2+deb13u1.dsc: no acceptable signature found diff -Nru python-daphne-4.1.2/debian/changelog python-daphne-4.1.2/debian/changelog --- python-daphne-4.1.2/debian/changelog 2024-08-18 17:46:43.000000000 +0000 +++ python-daphne-4.1.2/debian/changelog 2026-07-03 17:38:15.000000000 +0000 @@ -1,3 +1,13 @@ +python-daphne (4.1.2-2+deb13u1) trixie; urgency=medium + + * Non-maintainer upload. + * CVE-2026-44545: DoS via unbounded WebSocket message sizes + * CVE-2026-44546: Header injection on WebSocket upgrade path + * (Closes: #1138864) + + + -- Adrian Bunk Fri, 03 Jul 2026 20:38:15 +0300 + python-daphne (4.1.2-2) unstable; urgency=medium * Team upload. diff -Nru python-daphne-4.1.2/debian/patches/0001-Fixed-CVE-2026-44546-Prevent-header-injection-on-Web.patch python-daphne-4.1.2/debian/patches/0001-Fixed-CVE-2026-44546-Prevent-header-injection-on-Web.patch --- python-daphne-4.1.2/debian/patches/0001-Fixed-CVE-2026-44546-Prevent-header-injection-on-Web.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-daphne-4.1.2/debian/patches/0001-Fixed-CVE-2026-44546-Prevent-header-injection-on-Web.patch 2026-07-03 17:36:46.000000000 +0000 @@ -0,0 +1,154 @@ +From c69f1a75efa90c423f1a93f329fd8d6814e221a3 Mon Sep 17 00:00:00 2001 +From: Carlton Gibson +Date: Wed, 6 May 2026 09:23:37 +0200 +Subject: Fixed CVE-2026-44546: Prevent header injection on WebSocket upgrade + path +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Fixed a header injection vulnerability on the WebSocket upgrade path +(CVE-2026-44546). + +Header values containing ``\x0b``, ``\x0c``, ``\x1c``, ``\x1d``, ``\x1e``, or +``\x85`` were parsed as a single header by Twisted but split into multiple +headers by autobahn during the WebSocket handshake. An attacker could exploit +this parser differential to smuggle additional headers (e.g. authentication +tokens, ``X-Forwarded-For``, ``Origin``, ``Daphne-Root-Path``) into the ASGI +scope passed to the application. + +Daphne now rejects requests carrying these bytes in any header value with a 400 +Bad Request response, as required by RFC 9110 §5.5. + +Thanks to Rene Henningsen for the report. +--- + daphne/http_protocol.py | 12 ++++++-- + daphne/utils.py | 3 ++ + tests/test_websocket.py | 64 +++++++++++++++++++++++++++++++++++++++++ + 3 files changed, 76 insertions(+), 3 deletions(-) + +diff --git a/daphne/http_protocol.py b/daphne/http_protocol.py +index e9eba96..1319f46 100755 +--- a/daphne/http_protocol.py ++++ b/daphne/http_protocol.py +@@ -9,7 +9,7 @@ from twisted.protocols.policies import ProtocolWrapper + from twisted.web import http + from zope.interface import implementer + +-from .utils import HEADER_NAME_RE, parse_x_forwarded_for ++from .utils import HEADER_NAME_RE, INVALID_HEADER_VALUE_BYTES, parse_x_forwarded_for + + logger = logging.getLogger(__name__) + +@@ -70,11 +70,17 @@ class WebRequest(http.Request): + try: + self.request_start = time.time() + +- # Validate header names. +- for name, _ in self.requestHeaders.getAllRawHeaders(): ++ # Validate header names and values. ++ for name, values in self.requestHeaders.getAllRawHeaders(): + if not HEADER_NAME_RE.fullmatch(name): + self.basic_error(400, b"Bad Request", "Invalid header name") + return ++ for value in values: ++ if INVALID_HEADER_VALUE_BYTES.intersection(value): ++ self.basic_error( ++ 400, b"Bad Request", "Invalid header value" ++ ) ++ return + + # Get upgrade header + upgrade_header = None +diff --git a/daphne/utils.py b/daphne/utils.py +index 0699314..b634cd0 100644 +--- a/daphne/utils.py ++++ b/daphne/utils.py +@@ -7,6 +7,9 @@ from twisted.web.http_headers import Headers + # https://github.com/python-hyper/h11/blob/a2c68948accadc3876dffcf979d98002e4a4ed27/h11/_abnf.py#L10-L21 + HEADER_NAME_RE = re.compile(rb"[-!#$%&'*+.^_`|~0-9a-zA-Z]+") + ++# Disallowed in field-value per RFC 9110 §5.5. ++INVALID_HEADER_VALUE_BYTES = frozenset(b"\x0b\x0c\x1c\x1d\x1e\x85") ++ + + def import_by_path(path): + """ +diff --git a/tests/test_websocket.py b/tests/test_websocket.py +index 851143c..3ffbdb8 100644 +--- a/tests/test_websocket.py ++++ b/tests/test_websocket.py +@@ -306,6 +306,70 @@ class TestWebsocket(DaphneTestCase): + ) + + ++class TestHeaderValueInjection(DaphneTestCase): ++ """ ++ Twisted's bytes HTTP parser does not treat \\x0b, \\x0c, \\x1c, \\x1d, \\x1e ++ or \\x85 as line separators, but autobahn's WebSocket handshake parser ++ decodes to str and calls splitlines(), which does. Without rejection at ++ the Daphne edge, an attacker can smuggle additional headers into the ++ WebSocket ASGI scope through a single header value. Reject these bytes ++ on both paths so values can never reach a downstream str-based parser. ++ """ ++ ++ INVALID_BYTES = ( ++ b"\x0b", # vertical tab ++ b"\x0c", # form feed ++ b"\x1c", # file separator ++ b"\x1d", # group separator ++ b"\x1e", # record separator ++ b"\x85", # NEL ++ ) ++ ++ def _websocket_upgrade_request(self, value): ++ return ( ++ b"GET /ws HTTP/1.1\r\n" ++ b"Host: example.com\r\n" ++ b"Upgrade: websocket\r\n" ++ b"Connection: Upgrade\r\n" ++ b"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" ++ b"Sec-WebSocket-Version: 13\r\n" ++ b"X-Padding: " + value + b"\r\n" ++ b"\r\n" ++ ) ++ ++ def _http_request(self, value): ++ return ( ++ b"GET / HTTP/1.1\r\n" ++ b"Host: example.com\r\n" ++ b"X-Padding: " + value + b"\r\n" ++ b"\r\n" ++ ) ++ ++ def test_websocket_upgrade_rejects_smuggled_headers(self): ++ for byte in self.INVALID_BYTES: ++ with self.subTest(byte=byte): ++ value = b"innocent" + byte + b"X-Secret-Auth: admin-token" ++ response = self.run_daphne_raw( ++ self._websocket_upgrade_request(value) ++ ) ++ self.assertTrue( ++ response.startswith(b"HTTP/1.1 400"), ++ f"expected 400 for byte {byte!r}, got {response[:80]!r}", ++ ) ++ # Confirm the smuggled header didn't slip past validation. ++ self.assertNotIn(b"X-Secret-Auth", response) ++ ++ def test_http_request_rejects_invalid_header_value_bytes(self): ++ for byte in self.INVALID_BYTES: ++ with self.subTest(byte=byte): ++ value = b"innocent" + byte + b"injected" ++ response = self.run_daphne_raw(self._http_request(value)) ++ self.assertTrue( ++ response.startswith(b"HTTP/1.1 400"), ++ f"expected 400 for byte {byte!r}, got {response[:80]!r}", ++ ) ++ ++ + async def cancelling_application(scope, receive, send): + import asyncio + +-- +2.47.3 + diff -Nru python-daphne-4.1.2/debian/patches/0002-Fixed-CVE-2026-44545-Limit-WebSocket-sizes-in-autoba.patch python-daphne-4.1.2/debian/patches/0002-Fixed-CVE-2026-44545-Limit-WebSocket-sizes-in-autoba.patch --- python-daphne-4.1.2/debian/patches/0002-Fixed-CVE-2026-44545-Limit-WebSocket-sizes-in-autoba.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-daphne-4.1.2/debian/patches/0002-Fixed-CVE-2026-44545-Limit-WebSocket-sizes-in-autoba.patch 2026-07-03 17:36:46.000000000 +0000 @@ -0,0 +1,212 @@ +From 23e39f0c82ab8e7cb08ca7685a86af6dbd009097 Mon Sep 17 00:00:00 2001 +From: Carlton Gibson +Date: Wed, 6 May 2026 08:50:22 +0200 +Subject: 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. +--- + daphne/cli.py | 18 +++++++++++ + daphne/server.py | 6 ++++ + daphne/testing.py | 15 ++++++++- + tests/test_websocket.py | 69 +++++++++++++++++++++++++++++++++++++++++ + 4 files changed, 107 insertions(+), 1 deletion(-) + +diff --git a/daphne/cli.py b/daphne/cli.py +index a036821..0c88a9c 100755 +--- a/daphne/cli.py ++++ b/daphne/cli.py +@@ -107,6 +107,22 @@ class CommandLineInterface: + help="The number of seconds before a WebSocket is closed if no response to a keepalive ping", + default=30, + ) ++ self.parser.add_argument( ++ "--websocket-max-message-size", ++ type=int, ++ help="Maximum size, in bytes, of an incoming WebSocket message. " ++ "0 disables the limit (not recommended; allows unauthenticated " ++ "memory exhaustion).", ++ default=1024 * 1024, ++ ) ++ self.parser.add_argument( ++ "--websocket-max-frame-size", ++ type=int, ++ help="Maximum size, in bytes, of a single incoming WebSocket frame. " ++ "0 disables the limit (not recommended; allows unauthenticated " ++ "memory exhaustion).", ++ default=1024 * 1024, ++ ) + self.parser.add_argument( + "--application-close-timeout", + type=int, +@@ -269,6 +285,8 @@ class CommandLineInterface: + websocket_timeout=args.websocket_timeout, + websocket_connect_timeout=args.websocket_connect_timeout, + websocket_handshake_timeout=args.websocket_connect_timeout, ++ websocket_max_message_size=args.websocket_max_message_size, ++ websocket_max_frame_size=args.websocket_max_frame_size, + application_close_timeout=args.application_close_timeout, + action_logger=( + AccessLogGenerator(access_log_stream) if access_log_stream else None +diff --git a/daphne/server.py b/daphne/server.py +index a6d3819..4225046 100755 +--- a/daphne/server.py ++++ b/daphne/server.py +@@ -63,6 +63,8 @@ class Server: + proxy_forwarded_proto_header=None, + verbosity=1, + websocket_handshake_timeout=5, ++ websocket_max_message_size=1024 * 1024, ++ websocket_max_frame_size=1024 * 1024, + application_close_timeout=10, + ready_callable=None, + server_name="daphne", +@@ -83,6 +85,8 @@ class Server: + self.websocket_timeout = websocket_timeout + self.websocket_connect_timeout = websocket_connect_timeout + self.websocket_handshake_timeout = websocket_handshake_timeout ++ self.websocket_max_message_size = websocket_max_message_size ++ self.websocket_max_frame_size = websocket_max_frame_size + self.application_close_timeout = application_close_timeout + self.root_path = root_path + self.verbosity = verbosity +@@ -104,6 +108,8 @@ class Server: + autoPingTimeout=self.ping_timeout, + allowNullOrigin=True, + openHandshakeTimeout=self.websocket_handshake_timeout, ++ maxMessagePayloadSize=self.websocket_max_message_size, ++ maxFramePayloadSize=self.websocket_max_frame_size, + ) + if self.verbosity <= 1: + # Redirect the Twisted log to nowhere +diff --git a/daphne/testing.py b/daphne/testing.py +index 785edf9..6a426c8 100644 +--- a/daphne/testing.py ++++ b/daphne/testing.py +@@ -18,12 +18,21 @@ class BaseDaphneTestingInstance: + startup_timeout = 2 + + def __init__( +- self, xff=False, http_timeout=None, request_buffer_size=None, *, application ++ self, ++ xff=False, ++ http_timeout=None, ++ request_buffer_size=None, ++ websocket_max_message_size=None, ++ websocket_max_frame_size=None, ++ *, ++ application, + ): + self.xff = xff + self.http_timeout = http_timeout + self.host = "127.0.0.1" + self.request_buffer_size = request_buffer_size ++ self.websocket_max_message_size = websocket_max_message_size ++ self.websocket_max_frame_size = websocket_max_frame_size + self.application = application + + def get_application(self): +@@ -41,6 +50,10 @@ class BaseDaphneTestingInstance: + kwargs["proxy_forwarded_proto_header"] = "X-Forwarded-Proto" + if self.http_timeout: + kwargs["http_timeout"] = self.http_timeout ++ if self.websocket_max_message_size is not None: ++ kwargs["websocket_max_message_size"] = self.websocket_max_message_size ++ if self.websocket_max_frame_size is not None: ++ kwargs["websocket_max_frame_size"] = self.websocket_max_frame_size + # Start up process + self.process = DaphneProcess( + host=self.host, +diff --git a/tests/test_websocket.py b/tests/test_websocket.py +index 3ffbdb8..844b14e 100644 +--- a/tests/test_websocket.py ++++ b/tests/test_websocket.py +@@ -267,6 +267,75 @@ class TestWebsocket(DaphneTestCase): + "bytes": b"what is here? \xe2", + } + ++ def assert_oversized_frame_rejected(self, test_app): ++ """ ++ Sends a 16-byte text frame and asserts the application sees only ++ connect + disconnect — i.e. autobahn dropped the connection (its ++ default failByDrop behaviour) before dispatching the payload. ++ """ ++ test_app.add_send_messages([{"type": "websocket.accept"}]) ++ sock, _ = self.websocket_handshake(test_app) ++ _, messages = test_app.get_received() ++ self.assert_valid_websocket_connect_message(messages[0]) ++ self.websocket_send_frame(sock, "x" * 16) ++ deadline = time.time() + 2 ++ final_messages = [] ++ while time.time() < deadline: ++ _, final_messages = test_app.get_received() ++ if any(m["type"] == "websocket.disconnect" for m in final_messages): ++ break ++ time.sleep(0.05) ++ try: ++ sock.close() ++ except OSError: ++ pass ++ types = [m["type"] for m in final_messages] ++ self.assertEqual( ++ types, ++ ["websocket.connect", "websocket.disconnect"], ++ "Oversized frame should not have been delivered to the " ++ f"application, but got: {types}", ++ ) ++ ++ def test_websocket_max_message_size(self): ++ """ ++ Tests that an incoming WebSocket message exceeding ++ ``websocket_max_message_size`` is rejected by autobahn before it ++ reaches the application. ++ """ ++ # 16-byte frame > 8-byte message limit. ++ with DaphneTestingInstance(websocket_max_message_size=8) as test_app: ++ self.assert_oversized_frame_rejected(test_app) ++ ++ def test_websocket_max_frame_size(self): ++ """ ++ Tests that an incoming WebSocket frame exceeding ++ ``websocket_max_frame_size`` is rejected by autobahn before it ++ reaches the application, independently of the message size limit. ++ """ ++ # Large message limit, so the frame size limit is what trips. ++ with DaphneTestingInstance( ++ websocket_max_frame_size=8, ++ websocket_max_message_size=1024 * 1024, ++ ) as test_app: ++ self.assert_oversized_frame_rejected(test_app) ++ ++ def test_websocket_max_message_size_allows_under_limit(self): ++ """ ++ Tests that messages under ``websocket_max_message_size`` are ++ delivered to the application unchanged. ++ """ ++ with DaphneTestingInstance(websocket_max_message_size=64) as test_app: ++ test_app.add_send_messages([{"type": "websocket.accept"}]) ++ sock, _ = self.websocket_handshake(test_app) ++ _, messages = test_app.get_received() ++ self.assert_valid_websocket_connect_message(messages[0]) ++ test_app.add_send_messages( ++ [{"type": "websocket.send", "text": "ack"}] ++ ) ++ self.websocket_send_frame(sock, "x" * 16) ++ assert self.websocket_receive_frame(sock) == "ack" ++ + def test_http_timeout(self): + """ + Tests that the HTTP timeout doesn't kick in for WebSockets +-- +2.47.3 + diff -Nru python-daphne-4.1.2/debian/patches/series python-daphne-4.1.2/debian/patches/series --- python-daphne-4.1.2/debian/patches/series 2024-08-18 17:46:43.000000000 +0000 +++ python-daphne-4.1.2/debian/patches/series 2026-07-03 17:38:13.000000000 +0000 @@ -1 +1,3 @@ twisted-24.7.0.patch +0001-Fixed-CVE-2026-44546-Prevent-header-injection-on-Web.patch +0002-Fixed-CVE-2026-44545-Limit-WebSocket-sizes-in-autoba.patch