T05 · Unauthorized Access and Privilege Escalation
Error
- Location
- scripts/audit.py:347
- Finding
- Scan-root escape through symbolic-link traversal<![CDATA[ ## Vulnerability Details **File Location**: `scripts/audit.py:347-355` and `scripts/audit.py:538-543` **Vulnerability Type**: Filesystem boundary violation through symbolic-link following **Risk Level**: High ### Vulnerable Code ```python for path in self.root.rglob('*'): if not path.is_file(): continue # Prevent infinite loops from symlinks try: real_path = path.resolve() if real_path in self._visited: continue self._visited.add(real_path) except (OSError, ValueError): pass rel = path.relative_to(self.root) ``` The resulting path is subsequently read without verifying that its resolved target remains under the scan root: ```python for rel, path in self.walker.get_text_files(): try: content = path.read_text(encoding='utf-8-sig') except UnicodeDecodeError: try: content = path.read_text(encoding='latin-1') except Exception: continue ``` ### Technical Analysis `Path.is_file()` and `Path.read_text()` follow symbolic links. Although the walker resolves each path, it uses the resolved value only to detect duplicate targets. It never compares `real_path` with the resolved audit root. Consequently, a Skill directory can contain a file-shaped symbolic link whose target is outside the directory. The scanner treats the link as an in-scope file and reads the external target using the scanner process's host privileges. Static findings include up to 80 characters from matching source lines in report messages. Sensitive fragments from an external file may therefore be copied into terminal output, JSON, HTML, or SARIF reports and subsequently stored in CI artifacts. ### Attack Path 1. An attacker creates a Skill containing a symbolic link such as `config.py` pointing to a predictable host file. 2. A victim downloads or extracts the Skill in a way that preserves symbolic links. 3. The victim runs the scanner against the malicious S ...[truncated 724 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions - Resolve the audit root once with `root = self.root.resolve(strict=True)`. - Reject symbolic links by default with `path.is_symlink()` unless link scanning is an explicit requirement. - Before every read or hash operation, resolve the candidate and verify containment: ```python root = self.root.resolve(strict=True) candidate = path.resolve(strict=True) try: candidate.relative_to(root) except ValueError: self.skipped_files.append((str(path), 'outside_scan_root')) continue ``` - On supported Python versions, `candidate.is_relative_to(root)` may be used. - Apply the same containment policy to static scanning, taint tracking, fingerprint scanning, dependency manifests, and dynamic-scan entrypoint discovery. - Do not place matching source text in reports when the source may contain secrets. Redact credential-like values and provide only rule identifiers and locations. - Add regression tests covering file symlinks, directory symlinks, chained links, broken links, and links to sensitive files outside the root. ]]>
