fix(security): fix file_tools.py validate_path startswith bypass #7478 #7801

Merged
HAL9000 merged 2 commits from fix/issue-7478-validate-path-startswith into master 2026-05-15 00:54:22 +00:00
2 changed files with 28 additions and 12 deletions
@@ -11,7 +11,6 @@ from __future__ import annotations
import os
import re
from datetime import UTC, datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
import structlog
@@ -481,7 +480,15 @@ class LLMExecuteActor:
sandbox_root: str,
llm_output: str,
) -> None:
"""Write generated file contents to the sandbox directory."""
"""Write generated file contents to the sandbox directory.
Uses semantic path containment via os.path.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.
See issue #7478 — startswith bypass in path containment checks.
"""
pattern = re.compile(
r"FILE:\s*(.+?)\s*\n```[^\n]*\n(.*?)```",
@@ -491,17 +498,14 @@ class LLMExecuteActor:
for match in pattern.finditer(llm_output):
path = match.group(1).strip()
content = match.group(2)
full_path = Path(os.path.normpath(os.path.join(sandbox_root, path)))
# Path traversal guard: use relative_to (not string prefix) to
# avoid the *prefix-collision bypass* on symlink resolution.
sandbox_root_resolved = Path(sandbox_root).resolve()
try:
full_path.relative_to(sandbox_root_resolved)
except ValueError:
full_path = os.path.normpath(os.path.join(sandbox_root, path))
rel = os.path.relpath(full_path, sandbox_root)
# Path traversal guard: reject paths escaping sandbox
if rel.startswith(".." + os.sep) or rel == "..":
logger.warning(
"Rejected path traversal in LLM output",
path=path,
resolved=str(full_path),
resolved=full_path,
)
continue
os.makedirs(os.path.dirname(full_path), exist_ok=True)
+14 -2
View File
@@ -161,10 +161,22 @@ 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 equal to or a child of *root*.
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.
See issue #7478 — startswith bypass in path containment checks.
"""
if path == root:
return True
return path.startswith(root + "/")
try:
relative = posixpath.relpath(path, root)
except (ValueError, TypeError):
return False
return not relative.startswith(".." + posixpath.sep) and relative != ".."
def _relative_to(path: str, root: str) -> str: