T05 · Unauthorized Access and Privilege Escalation
Warning
- Location
- scripts/agent.py:78
- Finding
- Local skill scanning follows symbolic links outside the requested directory## Vulnerability Details **File Location**: `scripts/agent.py:78-99` **Vulnerability Type**: Improper symbolic-link handling and scan-root boundary violation **Risk Level**: Medium **Vulnerable 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 files ``` ### Technical 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()` and `open()` 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
- ## 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.
