Path handling / potential path traversal edge-case

MEDIUM
django/django
Commit: b33c31d99259
Affected: 5.1.x (stable/5.1.x) prior to this commit b33c31d992591bc8e8d20ac156809e4ae5b45375
2026-04-05 13:21 UTC

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