Path handling / potential path traversal edge-case
Description
The commit fixes an edge-case in ASGIRequest.path_info calculation. Previously, the code derived path_info by simply removing the script_name prefix from the request path (using removeprefix). This caused incorrect path_info for request paths that start with the script_name but do not have a slash boundary immediately after it. Example: path "/rootprefix/somepath/" with script_name "/root" would incorrectly strip "/root" and yield "prefix/somepath/". The new logic only strips the script_name when the path starts with script_name + "/" or when the path exactly equals script_name. Otherwise, it preserves the full path. This prevents miscomputation of path_info that could lead to unintended access to resources or leakage of path data, addressing a path handling vulnerability.
Proof of Concept
PoC demonstrating the difference in path_info calculation between the old (pre-fix) and new (post-fix) logic:
# Old logic (simulated removeprefix behavior)
def simulate_old(path, script_name):
if path.startswith(script_name):
return path[len(script_name):]
return path
# New logic (post-fix)
def simulate_new(path, script_name):
script_name = script_name.rstrip("/")
if path.startswith(script_name + "/") or path == script_name:
return path[len(script_name):]
else:
return path
path = "/rootprefix/somepath/"
script_name = "/root"
print("old =>", simulate_old(path, script_name)) # expected: "prefix/somepath/"
print("new =>", simulate_new(path, script_name)) # expected: "/rootprefix/somepath/"
# Explanation:
# - Old behavior strips the script_name even when the path does not have a boundary ("/root" followed by non-slash).
# - New behavior only strips when a slash boundary follows ("/root/") or the path equals the script_name.
# - In this boundary case, the old behavior could reveal or misroute paths like "prefix/somepath/" instead of the full path, potentially affecting routing or access decisions.
Commit Details
Author: khadyottakale
Date: 2026-02-22 08:58 UTC
Message:
Fixed #36940 -- Fixed script name edge case in ASGIRequest.path_info.
Paths that happened to begin with the script name were inappropriately
stripped, instead of checking that script name preceded a slash.
Triage Assessment
Vulnerability Type: Path Traversal
Confidence: MEDIUM
Reasoning:
The patch corrects how ASGI request path_info is derived from script_name to avoid incorrect stripping of the path. Miscomputing path_info could lead to unintended access to resources or leakage of path data, constituting a path handling vulnerability. The tests added cover boundary conditions to prevent regressions.
Verification Assessment
Vulnerability Type: Path handling / potential path traversal edge-case
Confidence: MEDIUM
Affected Versions: 5.1.x (stable/5.1.x) prior to this commit b33c31d992591bc8e8d20ac156809e4ae5b45375
Code Diff
diff --git a/django/core/handlers/asgi.py b/django/core/handlers/asgi.py
index c8118e1691f9..9555860a7e21 100644
--- a/django/core/handlers/asgi.py
+++ b/django/core/handlers/asgi.py
@@ -54,10 +54,13 @@ def __init__(self, scope, body_file):
self.path = scope["path"]
self.script_name = get_script_prefix(scope)
if self.script_name:
- # TODO: Better is-prefix checking, slash handling?
- self.path_info = scope["path"].removeprefix(self.script_name)
+ script_name = self.script_name.rstrip("/")
+ if self.path.startswith(script_name + "/") or self.path == script_name:
+ self.path_info = self.path[len(script_name) :]
+ else:
+ self.path_info = self.path
else:
- self.path_info = scope["path"]
+ self.path_info = self.path
# HTTP basics.
self.method = self.scope["method"].upper()
# Ensure query string is encoded correctly.
diff --git a/tests/handlers/tests.py b/tests/handlers/tests.py
index 83dfd95713b8..625090a66d0c 100644
--- a/tests/handlers/tests.py
+++ b/tests/handlers/tests.py
@@ -346,6 +346,26 @@ def test_force_script_name(self):
self.assertEqual(request.script_name, "/FORCED_PREFIX")
self.assertEqual(request.path_info, "/somepath/")
+ def test_root_path_prefix_boundary(self):
+ async_request_factory = AsyncRequestFactory()
+ # When path shares a textual prefix with root_path but not at a
+ # segment boundary, path_info should be the full path.
+ request = async_request_factory.request(
+ **{"path": "/rootprefix/somepath/", "root_path": "/root"}
+ )
+ self.assertEqual(request.path, "/rootprefix/somepath/")
+ self.assertEqual(request.script_name, "/root")
+ self.assertEqual(request.path_info, "/rootprefix/somepath/")
+
+ def test_root_path_trailing_slash(self):
+ async_request_factory = AsyncRequestFactory()
+ request = async_request_factory.request(
+ **{"path": "/root/somepath/", "root_path": "/root/"}
+ )
+ self.assertEqual(request.path, "/root/somepath/")
+ self.assertEqual(request.script_name, "/root/")
+ self.assertEqual(request.path_info, "/somepath/")
+
async def test_sync_streaming(self):
response = await self.async_client.get("/streaming/")
self.assertEqual(response.status_code, 200)