티스토리 수익 글 보기

티스토리 수익 글 보기

[4.2.x] Fixed CVE-2026-3902 — Ignored headers with underscores in AS… · django/django@4412731 · GitHub
Skip to content

Commit 4412731

Browse files
[4.2.x] 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 caf90a9 from main.
1 parent 8d2a05c commit 4412731

4 files changed

Lines changed: 38 additions & 1 deletion

File tree

django/core/handlers/asgi.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ def __init__(self, scope, body_file):
8585
_headers = defaultdict(list)
8686
for name, value in self.scope.get("headers", []):
8787
name = name.decode("latin1")
88+
# Prevent spoofing via ambiguity between underscores and hyphens.
89+
if "_" in name:
90+
continue
8891
if name == "content-length":
8992
corrected_name = "CONTENT_LENGTH"
9093
elif name == "content-type":

django/test/client.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,10 @@ def generic(
705705
if headers:
706706
extra.update(HttpHeaders.to_asgi_names(headers))
707707
s["headers"] += [
708-
(key.lower().encode("ascii"), value.encode("latin1"))
708+
# Avoid breaking test clients that just want to supply normalized
709+
# ASGI names, regardless of the fact that ASGIRequest drops headers
710+
# with underscores (CVE-2026-3902).
711+
(key.lower().replace("_", "-").encode("ascii"), value.encode("latin1"))
709712
for key, value in extra.items()
710713
]
711714
# If QUERY_STRING is absent or empty, we want to extract it from the

docs/releases/4.2.30.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,23 @@ Django 4.2.30 release notes
66

77
Django 4.2.30 fixes one security issue with severity "moderate" and four
88
security issues with severity "low" in 4.2.29.
9+
10+
CVE-2026-3902: ASGI header spoofing via underscore/hyphen conflation
11+
====================================================================
12+
13+
``ASGIRequest`` normalizes header names following WSGI conventions, mapping
14+
hyphens to underscores. As a result, even in configurations where reverse
15+
proxies carefully strip security-sensitive headers named with hyphens, such a
16+
header could be spoofed by supplying a header named with underscores.
17+
18+
Under WSGI, it is the responsibility of the server or proxy to avoid ambiguous
19+
mappings. (Django's :djadmin:`runserver` was patched in :cve:`2015-0219`.) But
20+
under ASGI, there is not the same uniform expectation, even if many proxies
21+
protect against this under default configuration (including ``nginx`` via
22+
``underscores_in_headers off;``).
23+
24+
Headers containing underscores are now ignored by ``ASGIRequest``, matching the
25+
behavior of :pypi:`Daphne <daphne>`, the reference server for ASGI.
26+
27+
This issue has severity "low" according to the :ref:`Django security policy
28+
<security-disclosure>`.

tests/asgi/tests.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,17 @@ def META(self, value):
220220
self.assertEqual(len(request.headers["foo"].split(",")), 200_000)
221221
self.assertLessEqual(setitem_count, 100)
222222

223+
async def test_underscores_in_headers_ignored(self):
224+
scope = self.async_request_factory._base_scope(path="/", http_version="2.0")
225+
scope["headers"] = [(b"some_header", b"1")]
226+
request = ASGIRequest(scope, None)
227+
# No form of the header exists anywhere.
228+
self.assertNotIn("Some_Header", request.headers)
229+
self.assertNotIn("Some-Header", request.headers)
230+
self.assertNotIn("SOME_HEADER", request.META)
231+
self.assertNotIn("SOME-HEADER", request.META)
232+
self.assertNotIn("HTTP_SOME_HEADER", request.META)
233+
223234
async def test_untouched_request_body_gets_closed(self):
224235
application = get_asgi_application()
225236
scope = self.async_request_factory._base_scope(method="POST", path="/post/")

0 commit comments

Comments
 (0)