T05 · Unauthorized Access and Privilege Escalation
- Location
scripts/scan_leaks.py:111- Finding
Repository Symlink Traversal Allows Reads Outside the Audit Root
- Content
View full analysis
Vulnerability Details
File Locations:
scripts/scan_leaks.py:111-158scripts/scan_public_surface.py:112-185
Vulnerability Type: Improper symbolic-link handling and filesystem boundary violation
Risk Level: MediumVulnerable Code
From
scripts/scan_leaks.py:python def iter_files(root: Path) -> list[Path]: files: list[Path] = [] for path in root.rglob("*"): if not path.is_file(): continue if any(part in IGNORE_DIRS or part.startswith(".venv") for part in path.relative_to(root).parts[:-1]): continue if not is_scannable_text_file(path): continue if not under_size_limit(path): continue files.append(path) return sorted(files) def sanitize_snippet(line: str) -> str: snippet = line.strip()[:220] for pattern, replacement in SNIPPET_REDACTIONS: snippet = pattern.sub(replacement, snippet) snippet = SECRET_ENV_VALUE.sub(r"\g<name>\g<sep><redacted>", snippet) return snippet def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--root", required=True, help="Repo root to scan.") parser.add_argument("--out", required=True, help="Output JSON file.") args = parser.parse_args() root = Path(args.root).expanduser().resolve() findings: list[dict[str, object]] = [] for path in iter_files(root): try: lines = path.read_text(encoding="utf-8").splitlines() except (OSError, UnicodeDecodeError): continue for line_number, line in enumerate(lines, start=1): for code, severity, pattern in PATTERNS: if pattern.search(line): findings.append( { "code": code, "severity": severity, ...[truncated 6467 chars]- Remediation
View remediation
Remediation Suggestions
-
Reject symbolic links during repository traversal:
python if path.is_symlink(): continue -
Resolve every candidate and enforce containment before any
stat(),exists(), orread_text()operation:python root = root.resolve() def safe_repo_file(path: Path, root: Path) -> Path | None: if path.is_symlink(): return None try: resolved = path.resolve(strict=True) resolved.relative_to(root) except (OSError, ValueError): return None return resolved if resolved.is_file() else None -
Perform all reads through the validated resolved path rather than the original repository path.
-
Apply the same validation to directly constructed paths, including
README.md,SKILL.md,openai.yaml,SECURITY.md, and other documentation paths. -
Where supported, open files using operating-system options that prohibit following symbolic links, such as
O_NOFOLLOW, to reduce time-of-check/time-of-use risks. -
Do not place source-line snippets in reports unless necessary. If snippets are retained, use conservative redaction or provide only the finding type and location.
-
Add regression tests for:
- A file symlink targeting a file outside the repository.
- A directory symlink targeting an external directory.
- Directly named symlinks such as
README.mdandSKILL.md. - Broken and cyclic symbolic links.
- A symlink changed between validation and reading.
-
