T05 · Unauthorized Access and Privilege Escalation
- Location
scripts/agent.py:78- Finding
Local skill scanning follows symbolic links outside the requested directory
- Content
View full analysis
Vulnerability Details
File Location:
scripts/agent.py:78-99
Vulnerability Type: Improper symbolic-link handling and scan-root boundary violation
Risk Level: MediumVulnerable Code:
python def collect_skill_files(path): """Read scannable files from a skill directory. Only called during explicit 'scan' command, never during 'sync'.""" files = {} for root, _dirs, fnames in os.walk(path): for fname in sorted(fnames): if len(files) >= MAX_FILES: return files fpath = os.path.join(root, fname) rel = os.path.relpath(fpath, path) lower = fname.lower() # Check scannable if lower in ("makefile", "dockerfile", "skill.md"): pass # always include elif not any(lower.endswith(ext) for ext in SCANNABLE_EXTS): continue try: sz = os.path.getsize(fpath) if sz == 0 or sz > MAX_FILE_SIZE: continue files[rel] = open(fpath, errors="replace").read() except: continue return filesTechnical Analysis
The scanner does not reject symbolic links or verify that the canonical path of each file remains beneath the canonical scan root. Both
os.path.getsize()andopen()follow a file-level symbolic link by default.The extension validation is performed against the untrusted directory-entry name rather than the resolved target. Consequently, a skill can include a symlink named with an accepted extension, such as
report.txt, while its target is an unrelated file outside the skill directory.The current fallback path collects these contents but submits only a constructed GitHub URL to the API. Therefore, the reviewed code does not establish direct transmission of the collected external file. Nevertheless, an unauthorized local read ...[truncated 1285 chars]
- Remediation
View remediation
Remediation Suggestions
-
Reject file-level symbolic links before metadata access or reading:
python if os.path.islink(fpath): continue -
Resolve the scan root and each candidate path, then enforce containment:
python scan_root = os.path.realpath(path) resolved = os.path.realpath(fpath) try: if os.path.commonpath([scan_root, resolved]) != scan_root: continue except ValueError: continue -
Where supported, open files with no-follow semantics such as
os.O_NOFOLLOW, then read from the resulting descriptor. -
Validate the opened descriptor with
os.fstat()to reduce time-of-check to time-of-use race conditions. -
Refuse non-regular files, including devices, FIFOs, and sockets.
-
Add automated tests covering file symlinks, links escaping through parent directories, links to sensitive files, and links changed between validation and opening.
-
