T05 · Unauthorized Access and Privilege Escalation
Warning
- Location
- scripts/policy_check.py:154
- Finding
- Scan-Root Escape Through Symbolic Links<![CDATA[ ## Vulnerability Details **File Location**: `scripts/policy_check.py:154-170` **Vulnerability Type**: Improper symbolic-link handling and unauthorized file access **Risk Level**: Medium ### Vulnerable Code ```python def should_scan_file(path: Path) -> bool: if not path.is_file(): return False if path.stat().st_size > 1024 * 1024: return False if path.suffix.lower() in TEXT_EXTENSIONS: return True if path.name in {"Dockerfile", "Makefile"}: return True return path.suffix == "" def iter_files(target: Path) -> Iterable[Path]: if target.is_file(): if should_scan_file(target): yield target return for root, dirs, files in os.walk(target): dirs[:] = [d for d in dirs if d not in DEFAULT_IGNORE_DIRS] root_path = Path(root) for filename in files: candidate = root_path / filename if should_scan_file(candidate): yield candidate ``` The resulting path is subsequently read without a containment check: ```python def scan_file(path: Path, root: Path) -> List[Dict[str, object]]: findings: List[Dict[str, object]] = [] try: text = path.read_text(encoding="utf-8", errors="ignore") except OSError: return findings ``` ### Technical Analysis `Path.is_file()`, `Path.stat()`, and `Path.read_text()` follow symbolic links. The scanner does not reject symbolic links or resolve each candidate and verify that the resolved path remains beneath the requested scan root. Consequently, an untrusted repository can contain a file symlink whose name has a permitted text extension but whose target is outside the repository. The scanner will treat the symlink as an ordinary file and read the external target. This exceeds the least privilege required for the declared repository-scanning functionality: scanning a selected repository does not require access to arbitrary files elsewhere on the host. ### A ...[truncated 1361 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Reject symbolic links before inspecting or reading candidates: ```python if path.is_symlink(): return False ``` 2. Resolve the scan root and each candidate, then enforce containment: ```python root_resolved = root.resolve() candidate_resolved = candidate.resolve() try: candidate_resolved.relative_to(root_resolved) except ValueError: continue ``` 3. Where supported, open files with operating-system options that prevent symbolic-link following, reducing time-of-check/time-of-use race exposure. 4. Catch `OSError` around `is_file()`, `stat()`, `resolve()`, and containment checks so broken or inaccessible links do not terminate the scan. 5. Add regression tests covering: - Symlinks to files outside the scan root. - Symlinks to directories outside the scan root. - Broken symlinks. - Symlink chains. - A symlink replaced between validation and reading. ]]>
