diff --git a/src/cleveragents/tool/path_mapper.py b/src/cleveragents/tool/path_mapper.py index b451f5440..0380cb24c 100644 --- a/src/cleveragents/tool/path_mapper.py +++ b/src/cleveragents/tool/path_mapper.py @@ -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("\\", "/")