T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/hygiene.py:440
- Finding
- Symlink-Following Writes Can Escape the Workspace Boundary## Vulnerability Details **File Location**: `scripts/hygiene.py:440-451` **Vulnerability Type**: Symlink-following arbitrary file overwrite **Risk Level**: High ### Vulnerable Code ```python def write_report(result: AuditResult, report_date: dt.date, report_only: bool) -> Path: report_dir = result.workspace / "projects" / "system" if not report_dir.exists() and not report_only: report_dir.mkdir(parents=True, exist_ok=True) result.fixes.append("Created report directory: projects/system") elif not report_dir.exists(): report_dir.mkdir(parents=True, exist_ok=True) report_path = report_dir / "hygiene-{0}.md".format(report_date.isoformat()) report_path.write_text(render_report(result, report_date), encoding="utf-8") result.report_path = report_path return report_path ``` ### Technical Analysis The report destination is predictable and is written without checking whether the destination or any parent component is a symbolic link. Python's `Path.write_text()` follows symbolic links and truncates an existing target before writing. The workspace path is initially resolved, but paths constructed beneath it are not resolved and validated immediately before use. Therefore, resolving the top-level workspace does not prevent a malicious workspace entry such as `projects/system/hygiene-YYYY-MM-DD.md`, `projects/system`, or another parent component from redirecting the write outside the workspace. This operation occurs during every normal invocation, including report-only operation, and does not require `--fix`. ### Attack Path 1. An attacker supplies a workspace or can modify a workspace that the victim will audit. 2. The attacker creates `projects/system/` and places a symbolic link named `hygiene-YYYY-MM-DD.md`, using the expected execution date. 3. The symbolic link targets an arbitrary file outside the workspace that is writable by the victim. 4. The victim runs ...[truncated 949 chars]
- Remediation
- ## Remediation Suggestions - Reject a report destination if it already exists as a symbolic link or is not a regular file. - Resolve the intended destination and verify with `Path.relative_to()` or `os.path.commonpath()` that it remains beneath the resolved workspace. - Validate every path component because checking only the final filename does not prevent redirection through a symlinked parent directory. - Open the output with no-follow and exclusive-creation protections where available, such as `os.open()` with `O_NOFOLLOW`, and then write through the returned descriptor. - Use a safely created temporary file in a verified directory, flush and synchronize it as appropriate, and atomically replace the destination only after repeating boundary and file-type checks. - Document that untrusted workspaces must not be audited with elevated privileges.
