diff options
| author | Jake Howard <git@theorangeone.net> | 2026-01-14 15:25:45 +0000 |
|---|---|---|
| committer | Jacob Walls <jacobtylerwalls@gmail.com> | 2026-02-03 08:00:14 -0500 |
| commit | 972dbdd4f7f69e9c405e6fe12a1b90e4713c1611 (patch) | |
| tree | 052d5bf91f17028daeca71c77f35f0ddf0c8f42f | |
| parent | d72cc3be3be0bbebdcaea5a8c8106b4d6f2a32bd (diff) | |
[6.0.x] Fixed CVE-2025-14550 -- Optimized repeated header parsing in ASGI requests.
Thanks Jiyong Yang for the report, and Natalia Bidart, Jacob Walls, and
Shai Berger for reviews.
Backport of eb22e1d6d643360e952609ef562c139a100ea4eb from main.
| -rw-r--r-- | django/core/handlers/asgi.py | 11 | ||||
| -rw-r--r-- | docs/releases/4.2.28.txt | 12 | ||||
| -rw-r--r-- | docs/releases/5.2.11.txt | 12 | ||||
| -rw-r--r-- | docs/releases/6.0.2.txt | 12 | ||||
| -rw-r--r-- | tests/asgi/tests.py | 28 |
5 files changed, 69 insertions, 6 deletions
diff --git a/django/core/handlers/asgi.py b/django/core/handlers/asgi.py index b4056ca042..1ca9130a78 100644 --- a/django/core/handlers/asgi.py +++ b/django/core/handlers/asgi.py @@ -3,6 +3,7 @@ import logging import sys import tempfile import traceback +from collections import defaultdict from contextlib import aclosing from asgiref.sync import ThreadSensitiveContext, sync_to_async @@ -83,6 +84,7 @@ class ASGIRequest(HttpRequest): self.META["SERVER_NAME"] = "unknown" self.META["SERVER_PORT"] = "0" # Headers go into META. + _headers = defaultdict(list) for name, value in self.scope.get("headers", []): name = name.decode("latin1") if name == "content-length": @@ -96,11 +98,10 @@ class ASGIRequest(HttpRequest): value = value.decode("latin1") if corrected_name == "HTTP_COOKIE": value = value.rstrip("; ") - if "HTTP_COOKIE" in self.META: - value = self.META[corrected_name] + "; " + value - elif corrected_name in self.META: - value = self.META[corrected_name] + "," + value - self.META[corrected_name] = value + _headers[corrected_name].append(value) + if cookie_header := _headers.pop("HTTP_COOKIE", None): + self.META["HTTP_COOKIE"] = "; ".join(cookie_header) + self.META.update({name: ",".join(value) for name, value in _headers.items()}) # Pull out request encoding, if provided. self._set_content_type_params(self.META) # Directly assign the body file to be our stream. diff --git a/docs/releases/4.2.28.txt b/docs/releases/4.2.28.txt index 9f6d5cb152..67d398308c 100644 --- a/docs/releases/4.2.28.txt +++ b/docs/releases/4.2.28.txt @@ -17,3 +17,15 @@ allowed remote attackers to enumerate users via a timing attack. This issue has severity "low" according to the :ref:`Django security policy <security-disclosure>`. + +CVE-2025-14550: Potential denial-of-service vulnerability via repeated headers when using ASGI +============================================================================================== + +When receiving duplicates of a single header, ``ASGIRequest`` allowed a remote +attacker to cause a potential denial-of-service via a specifically created +request with multiple duplicate headers. The vulnerability resulted from +repeated string concatenation while combining repeated headers, which +produced super-linear computation resulting in service degradation or outage. + +This issue has severity "moderate" according to the :ref:`Django security +policy <security-disclosure>`. diff --git a/docs/releases/5.2.11.txt b/docs/releases/5.2.11.txt index f975e45166..1e5187d7ec 100644 --- a/docs/releases/5.2.11.txt +++ b/docs/releases/5.2.11.txt @@ -17,3 +17,15 @@ allowed remote attackers to enumerate users via a timing attack. This issue has severity "low" according to the :ref:`Django security policy <security-disclosure>`. + +CVE-2025-14550: Potential denial-of-service vulnerability via repeated headers when using ASGI +============================================================================================== + +When receiving duplicates of a single header, ``ASGIRequest`` allowed a remote +attacker to cause a potential denial-of-service via a specifically created +request with multiple duplicate headers. The vulnerability resulted from +repeated string concatenation while combining repeated headers, which +produced super-linear computation resulting in service degradation or outage. + +This issue has severity "moderate" according to the :ref:`Django security +policy <security-disclosure>`. diff --git a/docs/releases/6.0.2.txt b/docs/releases/6.0.2.txt index ba39f74082..a258259195 100644 --- a/docs/releases/6.0.2.txt +++ b/docs/releases/6.0.2.txt @@ -18,6 +18,18 @@ allowed remote attackers to enumerate users via a timing attack. This issue has severity "low" according to the :ref:`Django security policy <security-disclosure>`. +CVE-2025-14550: Potential denial-of-service vulnerability via repeated headers when using ASGI +============================================================================================== + +When receiving duplicates of a single header, ``ASGIRequest`` allowed a remote +attacker to cause a potential denial-of-service via a specifically created +request with multiple duplicate headers. The vulnerability resulted from +repeated string concatenation while combining repeated headers, which +produced super-linear computation resulting in service degradation or outage. + +This issue has severity "moderate" according to the :ref:`Django security +policy <security-disclosure>`. + Bugfixes ======== diff --git a/tests/asgi/tests.py b/tests/asgi/tests.py index bb1020dd47..6d74903e41 100644 --- a/tests/asgi/tests.py +++ b/tests/asgi/tests.py @@ -223,7 +223,7 @@ class ASGITest(SimpleTestCase): self.assertEqual(response_body["type"], "http.response.body") self.assertEqual(response_body["body"], b"Echo!") - async def test_create_request_error(self): + async def test_request_too_big_request_error(self): # Track request_finished signal. signal_handler = SignalHandler() request_finished.connect(signal_handler) @@ -254,6 +254,32 @@ class ASGITest(SimpleTestCase): signal_handler.calls[0]["thread"], threading.current_thread() ) + async def test_meta_not_modified_with_repeat_headers(self): + scope = self.async_request_factory._base_scope(path="/", http_version="2.0") + scope["headers"] = [(b"foo", b"bar")] * 200_000 + + setitem_count = 0 + + class InstrumentedDict(dict): + def __setitem__(self, *args, **kwargs): + nonlocal setitem_count + setitem_count += 1 + super().__setitem__(*args, **kwargs) + + class InstrumentedASGIRequest(ASGIRequest): + @property + def META(self): + return self._meta + + @META.setter + def META(self, value): + self._meta = InstrumentedDict(**value) + + request = InstrumentedASGIRequest(scope, None) + + self.assertEqual(len(request.headers["foo"].split(",")), 200_000) + self.assertLessEqual(setitem_count, 100) + async def test_cancel_post_request_with_sync_processing(self): """ The request.body object should be available and readable in view |
