T05 · Unauthorized Access and Privilege Escalation
- Location
scripts/redaction_common.py:63- Finding
Symbolic Links Can Escape the Selected Artifact Root
- Content
View full analysis
list[Path]: if root.is_file(): return [root] return [path for path in sorted(root.rglob("*")) if path.is_file()] def is_text_candidate(path: Path) -> bool: if path.suffix.lower() in TEXT_SUFFIXES: return True return path.name.lower() in {"license", "readme", ".gitignore"} def read_text_if_supported(path: Path) -> Optional[str]: if not is_text_candidate(path): return None try: return path.read_text(encoding="utf-8") except (UnicodeDecodeError, OSError): return None ``` The redaction workflow subsequently copies the content returned by this helper: ```python for path in iter_candidate_files(root): if out_dir.exists() and path_is_within(path, out_dir): continue rel_path = Path(path.name) if root.is_file() else path.relative_to(root) text = read_text_if_supported(path) if text is None: skipped_files.append({"file": str(rel_path), "reason": "binary-or-unsupported"}) continue redacted, counts = redact_text(text) merge_counts(totals, counts) target_path = out_dir / rel_path target_path.parent.mkdir(parents=True, exist_ok=True) target_path.write_text(redacted, encoding="utf-8") ``` ### Technical Analysis `Path.rglob()` discovers entries beneath the supplied root, but `Path.is_file()` and `Path.read_text()` follow symbolic links. The implementation does not reject symbolic links or resolve each candidate and verify that its resolved target remains inside the resolved artifact root. Consequently, an artifact directory can cont ...[truncated 2176 chars]- Remediation
View remediation
list[Path]: if root.is_symlink(): return [] if root.is_file(): return [root] return [ path for path in sorted(root.rglob("*")) if not path.is_symlink() and path.is_file() ] ``` 2. Independently resolve and validate every candidate immediately before reading it: ```python resolved_root = root.resolve(strict=True) resolved_candidate = path.resolve(strict=True) try: resolved_candidate.relative_to(resolved_root) except ValueError: # Record as a rejected link requiring manual review. continue ``` 3. Perform validation as close as possible to the file-open operation to reduce time-of-check/time-of-use exposure. Where supported, use non-following file-open facilities such as `os.open()` with `O_NOFOLLOW`, then read from the validated descriptor. 4. Apply the same policy when `--root` itself is a file, because the supplied root can also be a symbolic link. 5. Record rejected links as manual-review items rather than silently processing or omitting them. 6. Add tests covering links to files outside the root, links to directories, broken links, nested links, and replacement of a validated file with a link during processing. ]]>
