Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
python-django (3:4.2.27-2deepin1) unstable; urgency=medium

* Fix CVE-2026-3902: ASGI header spoofing via underscore/hyphen
conflation. ASGIRequest now ignores headers containing underscores
to prevent spoofing via ambiguity between underscores and hyphens.

-- deepin-ci-robot <packages@deepin.org> Wed, 15 Apr 2026 17:38:08 +0800

python-django (3:4.2.27-2) unstable; urgency=medium

* Team upload.
Expand Down
67 changes: 67 additions & 0 deletions debian/patches/CVE-2026-3902.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
From: Jacob Walls <jacobtylerwalls@gmail.com>
Date: Thu, 22 Jan 2026 17:01:46 -0500
Subject: [PATCH] Fixed CVE-2026-3902 -- Ignored headers with underscores in ASGIRequest.

Thanks Tarek Nakkouch for the report and Jake Howard and Natalia Bidart
for reviews.

Backport of caf90a971f09323775ed0cacf94eadaf39d040e0 from main.

---
django/core/handlers/asgi.py | 3 +++
django/test/client.py | 5 ++++-
tests/asgi/tests.py | 11 +++++++++++
3 files changed, 18 insertions(+), 1 deletion(-)

diff --git a/django/core/handlers/asgi.py b/django/core/handlers/asgi.py
index d9518231180d..3b10a59ec73b 100644
--- a/django/core/handlers/asgi.py
+++ b/django/core/handlers/asgi.py
@@ -85,6 +85,9 @@ def __init__(self, scope, body_file):
_headers = defaultdict(list)
for name, value in self.scope.get("headers", []):
name = name.decode("latin1")
+ # Prevent spoofing via ambiguity between underscores and hyphens.
+ if "_" in name:
+ continue
if name == "content-length":
corrected_name = "CONTENT_LENGTH"
elif name == "content-type":
diff --git a/django/test/client.py b/django/test/client.py
index cf63265faa92..a465cc98f55b 100644
--- a/django/test/client.py
+++ b/django/test/client.py
@@ -705,7 +705,10 @@ def generic(
if headers:
extra.update(HttpHeaders.to_asgi_names(headers))
s["headers"] += [
- (key.lower().encode("ascii"), value.encode("latin1"))
+ # Avoid breaking test clients that just want to supply normalized
+ # ASGI names, regardless of the fact that ASGIRequest drops headers
+ # with underscores (CVE-2026-3902).
+ (key.lower().replace("_", "-").encode("ascii"), value.encode("latin1"))
for key, value in extra.items()
]
# If QUERY_STRING is absent or empty, we want to extract it from the
diff --git a/tests/asgi/tests.py b/tests/asgi/tests.py
index 9395c8626cac..bf623c208389 100644
--- a/tests/asgi/tests.py
+++ b/tests/asgi/tests.py
@@ -220,6 +220,17 @@ def META(self, value):
self.assertEqual(len(request.headers["foo"].split(",")), 200_000)
self.assertLessEqual(setitem_count, 100)

+ async def test_underscores_in_headers_ignored(self):
+ scope = self.async_request_factory._base_scope(path="/", http_version="2.0")
+ scope["headers"] = [(b"some_header", b"1")]
+ request = ASGIRequest(scope, None)
+ # No form of the header exists anywhere.
+ self.assertNotIn("Some_Header", request.headers)
+ self.assertNotIn("Some-Header", request.headers)
+ self.assertNotIn("SOME_HEADER", request.META)
+ self.assertNotIn("SOME-HEADER", request.META)
+ self.assertNotIn("HTTP_SOME_HEADER", request.META)
+
async def test_untouched_request_body_gets_closed(self):
application = get_asgi_application()
scope = self.async_request_factory._base_scope(method="POST", path="/post/")
1 change: 1 addition & 0 deletions debian/patches/series
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ py314-test-runner-parallel.patch
py314-copy-BaseContext.patch
py314-test-prefetch-related-queryset.patch
test-strip-tags-incomplete-entities.patch
CVE-2026-3902.patch
Loading