T09 · Insecure Skill Coding Practices
- Location
scripts/profile_files.py:12- Finding
Service-Controlled Manifest Paths Can Escape the Home Directory Through Symbolic Links
- Content
View full analysis
Vulnerability Details
File Location:
scripts/profile_files.py, lines 12–18 and 21–29
Vulnerability Type: Symlink-based path-boundary bypass leading to unintended file disclosure
Risk Level: MediumComplete Code Snippet:
python def safe_relative(rel): """True for a home-relative path without escapes or absolute roots.""" text = str(rel) if text[:1] in ("/", "~") or chr(92) in text or ".." in text.split("/"): return False parts = [p for p in text.split("/") if p not in ("", ".")] return bool(parts) and len(parts) <= 8 def newest(manifest, max_bytes): """Return the first readable file from the manifest, size-capped.""" for rel in (manifest or {}).get("paths", []): if not safe_relative(rel): continue p = HOME / rel try: if p.is_file() and p.stat().st_size <= int(max_bytes): return p.read_bytes() except OSError: continue return NoneTechnical Analysis
The path validation is purely lexical. It rejects absolute paths, home-prefix notation, backslashes, and explicit
..components, but it does not resolve symbolic links before enforcing the documented home-directory boundary.The manifest is obtained from the sync service by
fetch_manifest()and passed fromscripts/setup.pytonewest(). A path such aslinked-directory/secretpassessafe_relative()even if~/linked-directoryis a symbolic link to a directory outside the user's home. Calls tois_file(),stat(), andread_bytes()follow symbolic links by default.Once read, the resulting bytes are passed to
push_snapshot()inscripts/setup.pyand transmitted to the local profile-sync daemon over its Unix-domain socket. The file-size cap limits the quantity of data per run but does not enforce the intended filesystem scope.The trust boundary is crossed when service-provided manife ...[truncated 1649 chars]
- Remediation
View remediation
Remediation Suggestions
Resolve the home directory and each candidate path before reading, then verify that the resolved candidate remains strictly inside the resolved home directory:
python home = HOME.resolve() candidate = (home / rel).resolve(strict=True) try: candidate.relative_to(home) except ValueError: continueDo not rely solely on string checks for filesystem containment. Also consider rejecting symbolic links in every path component.
To reduce time-of-check/time-of-use exposure, open files using descriptor-relative operating-system APIs with no-follow protections such as
O_NOFOLLOW, validate the opened descriptor withfstat(), enforce the size limit on that descriptor, and read from the same descriptor. Apply an explicit allowlist of intended profile directories or file names if the sync set has a known scope.
