T05 · Unauthorized Access and Privilege Escalation
- Location
auditor.py:94- Finding
Audit Root Escape Through Symbolic-Link File Reads
- Content
View full analysis
Vulnerability Details
File Location:
auditor.py, lines 94-101 and 127-134
Vulnerability Type: Symbolic-link path traversal and unauthorized file read
Risk Level: MediumVulnerable Code
python try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() except UnicodeDecodeError: # If it's not a text file, skip it return issues except Exception: # If we can't read the file for any reason, skip it return issuespython # Check all Python and shell files in the skill directory for file_path in skill_path.rglob("*"): if file_path.is_file() and (file_path.suffix in ['.py', '.sh', '.js', '.ts', '.json', '.yaml', '.yml'] or file_path.name in ['SKILL.md', 'skill.json']): issues = self.scan_file(str(file_path)) all_issues.extend(issues)Technical Analysis
The scanner recursively discovers files under a user-supplied Skill directory and opens each selected path without rejecting symbolic links. It also does not resolve the candidate path and verify that its target remains under the resolved audit root.
On common filesystems,
Path.is_file()follows symbolic links. Consequently, a symbolic link located inside the audited directory can point to a regular file outside that directory and still pass the file check.scan_file()then follows the link when callingopen().The external file is read using the auditor process's privileges. If its contents match a configured expression, the matching data is stored in
matched_textand may be exposed throughgenerate_report(). The weakness is constrained by the process's filesystem permissions and the scanner's filename or extension filters, but a link can be assigned an accepted name such asleak.py.Attack Path
- An attacker prepares a Skill directory that will be submitted for auditing.
- The attacker creates a symbolic ...[truncated 1126 chars]
- Remediation
View remediation
Remediation Suggestions
- Resolve the audit root once with
Path(skill_path).resolve(strict=True). - Reject every discovered path for which
file_path.is_symlink()is true. - Resolve each candidate and verify that it remains under the resolved root using
candidate.relative_to(root)inside exception handling. - Open files in a way that prevents link following where the platform supports it, such as
os.open()withO_NOFOLLOW. - Revalidate the opened file descriptor to reduce time-of-check/time-of-use race conditions.
- Run the auditor under a sandboxed, unprivileged account with access limited to the submitted directory.
- Avoid including raw matched content in reports. Prefer rule identifiers, line numbers, and redacted excerpts.
- Record rejected links as explicit scan errors rather than silently ignoring them.
- Resolve the audit root once with
