T05 · Unauthorized Access and Privilege Escalation
- Location
scripts/xss_audit.py:381- Finding
Recursive Scanner Follows Symbolic Links Outside the Selected Scan Root
- Content
View full analysis
Vulnerability Details
File Location:
scripts/xss_audit.py:153-158andscripts/xss_audit.py:381-386
Vulnerability Type: Unrestricted symbolic-link traversal / scan-boundary bypass
Risk Level: MediumVulnerable Code
python def read_lines(path: Path) -> list[str]: for enc in ("utf-8", "latin-1"): try: return path.read_text(encoding=enc).splitlines() except Exception: pass return []python def scan_directory(root: Path) -> list[Finding]: findings: list[Finding] = [] SKIP_DIRS = {".git", "node_modules", ".venv", "venv", "__pycache__", "dist", "build", ".next", "vendor", "coverage"} for path in root.rglob("*"): if not path.is_file(): continue if any(p in SKIP_DIRS for p in path.parts): continue findings.extend(scan_file(path)) return findingsTechnical Analysis
The recursive scanner does not reject symbolic links or verify that a discovered file resolves beneath the selected scan root. Both
Path.is_file()andPath.read_text()follow symbolic links.An attacker who controls files in a scanned repository can create a symbolic link with a supported source-code extension, such as
.py,.js, or.html, that points to a file elsewhere on the host. When the repository is scanned,root.rglob("*")discovers that link,path.is_file()follows it, andread_text()reads the external target.The extension check is performed against the link's filename rather than the resolved target. Therefore, a link named
external.pycan cause an arbitrary readable target to be processed even if the target itself has another extension.Attack Path
- An attacker adds a symbolic link to a repository or other directory that will be scanned.
- The link uses a supported extension and points outside the repository, for example:
text
...[truncated 1444 chars]
- Remediation
View remediation
Remediation Suggestions
-
Reject symbolic links before inspecting or reading candidates:
python if path.is_symlink(): continue -
Resolve the scan root and every candidate, then enforce containment:
python root_resolved = root.resolve() for path in root.rglob("*"): if path.is_symlink(): continue try: resolved = path.resolve(strict=True) resolved.relative_to(root_resolved) except (OSError, ValueError): continue if not resolved.is_file(): continue findings.extend(scan_file(resolved)) -
Perform containment validation immediately before opening the file to reduce time-of-check/time-of-use exposure. For stronger protection on supported platforms, open files without following symbolic links and validate the resulting file descriptor.
-
Only process regular files and impose a reasonable maximum file-size limit before calling
read_text()to reduce availability risks from oversized files. -
Add automated tests covering:
- A symlink to a file outside the scan root.
- A symlink whose name has a supported extension but whose target does not.
- Broken and circular symbolic links.
- Ordinary files inside the root.
- Files changed into symbolic links during scanning.
-
