T05 · Unauthorized Access and Privilege Escalation
Warning
- Location
- claw_asset_privacy_guardian.py:596
- Finding
- Scan-root boundary bypass through symbolic links<![CDATA[ ## Vulnerability Details **File Location**: `claw_asset_privacy_guardian.py:596-602` and `claw_asset_privacy_guardian.py:633-639` **Vulnerability Type**: Scan-root boundary violation through symbolic-link following **Risk Level**: Medium ### Vulnerable Code ```python for file_path in all_files: if not self._should_scan_file(file_path): continue try: with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() ``` ```python all_files = [] for root, dirs, files in os.walk(directory): dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', '__pycache__']] for file in files: file_path = os.path.join(root, file) all_files.append(file_path) ``` ### Technical Analysis The scanner constructs candidate paths from directory entries and subsequently opens each path without checking whether it is a symbolic link. It also does not resolve the candidate path and verify that the resolved target remains under the requested scan root. Although `os.walk()` does not traverse symbolic links to directories by default, symbolic links appearing as files are included in the `files` collection. Python's `open()` follows these links. Consequently, a supported file inside the scanned project can reference a file outside the authorized scan directory. The file extension check operates on the link's path rather than enforcing a boundary on its resolved target. For example, a link named `external.env` can point to a readable environment file elsewhere on the system and will be accepted for scanning. ### Attack Path 1. An attacker creates or contributes a project containing a symbolic link such as: ```text linked-secrets.env -> /home/victim/private/application.env ``` 2. The victim invokes the scanner on the attacker-controlled project directory. 3. `_collect_files()` includes `linked-secrets.env` in `all_files`. 4. `_should_scan_file()` accepts the path because `.env` i ...[truncated 1183 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Resolve the scan root once before traversal: ```python scan_root = Path(directory).resolve(strict=True) ``` 2. Resolve every candidate and verify that it remains under the scan root. For Python 3.8 compatibility, use `os.path.commonpath()`: ```python candidate = Path(file_path) if candidate.is_symlink(): logger.warning("Skipping symbolic link: %s", candidate) continue resolved = candidate.resolve(strict=True) if os.path.commonpath([str(scan_root), str(resolved)]) != str(scan_root): logger.warning("Skipping path outside scan root: %s", candidate) continue ``` 3. Reject symbolic links by default. If link scanning is required, expose an explicit opt-in option and still require resolved targets to remain inside the scan root. 4. Where supported, open files using no-follow semantics such as `os.open()` with `O_NOFOLLOW`, then wrap the descriptor with `os.fdopen()`. This reduces time-of-check/time-of-use exposure. 5. After opening a file, compare descriptor metadata against the previously validated file metadata when scanning directories writable by untrusted users. 6. Add regression tests covering: - A file symlink targeting a file outside the scan root. - A file symlink targeting a file inside the scan root. - Broken links. - Link replacement during scanning. - Nested paths whose normalized representation escapes the root. ]]>
