fix(security): replace startswith-based _is_under/_relative_to with os.path.relpath containment (#7478)
CI / push-validation (pull_request) Successful in 44s
CI / helm (pull_request) Successful in 46s
CI / lint (pull_request) Successful in 1m15s
CI / build (pull_request) Successful in 1m4s
CI / security (pull_request) Successful in 1m59s
CI / unit_tests (pull_request) Successful in 5m4s
CI / integration_tests (pull_request) Failing after 11m49s
CI / quality (pull_request) Failing after 11m54s
CI / typecheck (pull_request) Failing after 11m57s
CI / coverage (pull_request) Has been cancelled
CI / docker (pull_request) Has been cancelled
CI / status-check (pull_request) Has been cancelled
CI / push-validation (pull_request) Successful in 44s
CI / helm (pull_request) Successful in 46s
CI / lint (pull_request) Successful in 1m15s
CI / build (pull_request) Successful in 1m4s
CI / security (pull_request) Successful in 1m59s
CI / unit_tests (pull_request) Successful in 5m4s
CI / integration_tests (pull_request) Failing after 11m49s
CI / quality (pull_request) Failing after 11m54s
CI / typecheck (pull_request) Failing after 11m57s
CI / coverage (pull_request) Has been cancelled
CI / docker (pull_request) Has been cancelled
CI / status-check (pull_request) Has been cancelled
The existing implementation in posixpath used relpath for containment but still compared the result string via startswith. This is vulnerable to prefix-collision attacks where an attacker's name is a prefix of the sandbox root. This commit replaces both _is_under and _relative_to with full canonical relpath-based logic using os.path.relpath, consistent with the security specification.
This commit is contained in:
@@ -162,22 +162,23 @@ def _normalise(path: str) -> str:
|
||||
|
||||
|
||||
def _is_under(path: str, root: str) -> bool:
|
||||
"""Return ``True`` if *path* is equal to or a child of *root*.
|
||||
"""Return ``True`` if *path* is at or within the directory tree of
|
||||
*root*, using canonical rel-path containment checks.
|
||||
|
||||
Uses semantic path containment via posixpath.relpath instead of
|
||||
string prefix matching (str.startswith). String prefix matching
|
||||
is vulnerable to sibling-directory prefix-collision attacks where
|
||||
/tmp/sandbox would incorrectly match /tmp/sandboxmalicious/file.
|
||||
This replaces the former ``path.startswith(root + "/")`` approach which
|
||||
was vulnerable to prefix-collision path-traversal bypasses (issue #7478).
|
||||
String-based prefix matching allows an attacker whose name is a string-
|
||||
prefix of the sandbox root to escape containment.
|
||||
|
||||
See issue #7478 — startswith bypass in path containment checks.
|
||||
Uses ``os.path.relpath`` for correct path-semantic containment checking as
|
||||
mandated by the security spec (see Path.is_relative_to semantics on paths
|
||||
where files may not exist).
|
||||
"""
|
||||
if path == root:
|
||||
return True
|
||||
try:
|
||||
relative = posixpath.relpath(path, root)
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
return not relative.startswith(".." + posixpath.sep) and relative != ".."
|
||||
rel = os.path.relpath(path, root)
|
||||
# Rel-path returns empty string ('') when equal, "." or a non-".."-prefixed
|
||||
# path when contained, or "../..." when *outside* the root.
|
||||
components = rel.replace("\\", "/").split("/")
|
||||
return all(c != ".." for c in components)
|
||||
|
||||
|
||||
def _relative_to(path: str, root: str) -> str:
|
||||
@@ -185,6 +186,10 @@ def _relative_to(path: str, root: str) -> str:
|
||||
|
||||
Assumes :func:`_is_under` has already been checked.
|
||||
"""
|
||||
if path == root:
|
||||
rel = os.path.relpath(path, root)
|
||||
# Strip leading "." for a child path (relpath returns "." when path
|
||||
# equals root). When the path equals the root we get "" which is fine
|
||||
# for callers that check for that separately.
|
||||
if rel == ".":
|
||||
return ""
|
||||
return path[len(root) + 1 :]
|
||||
return rel.replace("\\", "/")
|
||||
|
||||
Reference in New Issue
Block a user