summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJake Howard <git@theorangeone.net>2026-01-14 15:25:45 +0000
committerJacob Walls <jacobtylerwalls@gmail.com>2026-02-03 08:00:14 -0500
commit972dbdd4f7f69e9c405e6fe12a1b90e4713c1611 (patch)
tree052d5bf91f17028daeca71c77f35f0ddf0c8f42f
parentd72cc3be3be0bbebdcaea5a8c8106b4d6f2a32bd (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.py11
-rw-r--r--docs/releases/4.2.28.txt12
-rw-r--r--docs/releases/5.2.11.txt12
-rw-r--r--docs/releases/6.0.2.txt12
-rw-r--r--tests/asgi/tests.py28
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