T05 · Unauthorized Access and Privilege Escalation
- Location
dependency_manager.py:27- Finding
Caller-Controlled Project Directory Enables Writes Outside the Intended Workspace
- Content
View full analysis
Vulnerability Details
File Location:
dependency_manager.py:27-42, with the corresponding graph write atdependency_manager.py:94-101
Vulnerability Type: Unrestricted filesystem path and symlink following
Risk Level: LowVulnerable Code
python def __init__(self, project_dir: str): self.project_dir = Path(project_dir) self.dependency_file = self.project_dir / "dependency_graph.json" self._lock_path = self.project_dir / ".dependency_lock" @contextmanager def _file_lock(self): """File lock context manager""" self.project_dir.mkdir(parents=True, exist_ok=True) with open(self._lock_path, 'w') as lock_file: try: fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX) yield finally: fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)The dependency graph is subsequently written using the path derived from the same unrestricted directory:
python def _save_dependency_graph(self, data: Dict): """Save dependency graph to file""" with self._file_lock(): try: with open(self.dependency_file, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False) logger.debug("Dependency graph saved to file") except Exception as e: logger.error(f"Failed to save dependency graph: {e}")Technical Analysis
The constructor accepts
project_dirand converts it directly to aPathwithout resolving it against an approved workspace root. The implementation then creates the supplied directory and opens.dependency_lockanddependency_graph.jsonin write mode.No validation establishes that:
- The resolved project directory is beneath an authorized workspace.
- The directory or its parent components are not symbolic links.
.dependency_lockanddependency_graph.jsonare regular files rather than symbolic links. ...[truncated 2610 chars]
- Remediation
View remediation
Remediation Suggestions
-
Define an explicit trusted workspace root and resolve both it and the requested project path before performing filesystem operations:
python workspace_root = Path(allowed_workspace).resolve(strict=True) project_dir = Path(project_dir).resolve(strict=False) if project_dir != workspace_root and workspace_root not in project_dir.parents: raise ValueError("Project directory is outside the authorized workspace") -
Reject absolute paths when the interface is intended to accept only workspace-relative project names.
-
Inspect existing path components with
lstat()and reject symbolic links. Repeat security-sensitive validation immediately before writing to reduce time-of-check/time-of-use exposure. -
Open lock and data files with flags that refuse symbolic links where supported, such as
os.open()withO_NOFOLLOW. Verify withfstat()that the opened object is a regular file. -
Write the graph atomically:
- Create a temporary file in the validated destination directory.
- Set restrictive permissions such as
0600. - Flush and call
fsync(). - Replace the destination with
os.replace()only after validation.
-
Centralize workspace confinement and safe-file-opening logic so that dependency, state, task, logging, and workflow components apply the same policy.
-
Run the workflow under a dedicated least-privileged account or isolated container whose filesystem access is limited to the authorized workspace.
-
Add tests covering absolute paths,
..traversal, symlinked parent directories, symlinked destination files, and replacement of paths during write operations.
-
