Denial of Service (DoS) via repeated headers in ASGI requests

HIGH
django/django
Commit: eb22e1d6d643
Affected: 5.1.x (stable/5.1.x) prior to this commit
2026-04-05 13:59 UTC

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.

Commit Details

Author: Jake Howard

Date: 2026-01-14 15:25 UTC

Message:

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.

Triage Assessment

Vulnerability Type: Denial of Service (DoS) via repeated headers in ASGI requests

Confidence: HIGH

Reasoning:

Commit description references CVE-2025-14550 and changes to ASGI header parsing to optimize handling of repeated headers. The diff shows restructuring of header aggregation to avoid repeated string concatenation and potential DoS via crafted header sets. Release notes explicitly describe a moderate severity DoS vulnerability from duplicate headers, and tests validate behavior with many repeated headers. This constitutes a security vulnerability fix for DoS due to header processing.

Verification Assessment

Vulnerability Type: Denial of Service (DoS) via repeated headers in ASGI requests

Confidence: HIGH

Affected Versions: 5.1.x (stable/5.1.x) prior to this commit

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
← Back to Alerts View on GitHub →