T05 · Unauthorized Access and Privilege Escalation
- Location
scripts/prompt_inventory.py:127- Finding
Scan-Root Escape Through Symbolic Links
- Content
View full analysis
Vulnerability Details
File Location:
scripts/prompt_inventory.py:127-133, 276-301, 320-345
Vulnerability Type: Filesystem scope bypass through unresolved symbolic-link trust
Risk Level: MediumVulnerable Code
python def expand_root(value: str | Path) -> Path: return Path(str(value)).expanduser().resolve() def posix_rel(path: Path, root: Path) -> str: try: return path.resolve().relative_to(root.resolve()).as_posix() except ValueError: return path.resolve().as_posix()python def candidate_paths(root: Path, mode: str, profile: str, includes: list[str], operator_edited_only: bool, operator_edited_patterns: list[str]) -> list[Path]: if root.is_file(): return [root] if operator_edited_only: patterns = operator_edited_patterns or OPERATOR_EDITED_PATTERNS elif mode == "shallow": patterns = SHALLOW_PATTERNS + PROFILE_PATTERNS.get(profile, []) else: patterns = [] if patterns: found: dict[str, Path] = {} for pattern in patterns: if pattern.startswith("~") or Path(pattern).is_absolute(): for path in concrete_paths([pattern]): found[str(path.resolve())] = path continue for path in root.glob(pattern): if path.is_file(): found[str(path.resolve())] = path return sorted(found.values(), key=lambda p: p.as_posix().lower()) try: return [path for path in root.rglob("*") if path.is_file()] except OSError: return []python for root_value in roots: root = expand_root(root_value) if not root.exists(): warnings.append(f"root_not_found: {root}") continue for path in candidate_paths(root, mode, profile, include_patterns, operator_edited_only, operator_patterns): ...[truncated 3491 chars]- Remediation
View remediation
Remediation Suggestions
-
Resolve every candidate before inspection and enforce containment:
python root_resolved = root.resolve() candidate_resolved = path.resolve(strict=True) try: candidate_resolved.relative_to(root_resolved) except ValueError: warnings.append(f"out_of_root_path_skipped: {path}") continue -
Reject symbolic links by default with
path.is_symlink(). If symlink support is necessary, require explicit opt-in and allow only links whose resolved targets remain under the authorized root. -
Apply exclusions and sensitive-path checks to both the visible link path and the resolved target path.
-
Change
posix_rel()to fail closed on containment failure rather than returning an absolute path. -
Open the validated resolved path and account for time-of-check/time-of-use races. On supported systems, consider descriptor-based opening with no-follow protections.
-
Add regression tests covering:
- A link to an out-of-root ordinary text file.
- A benignly named link to an out-of-root
.envor private-key file. - An in-root link to an in-root target.
- Broken links and symlink loops.
-
