T09 · Insecure Skill Coding Practices
- Location
scripts/hygiene.py:440- Finding
Symlink-Following Writes Can Escape the Workspace Boundary
- Content
View full analysis
Vulnerability Details
File Location:
scripts/hygiene.py:440-451
Vulnerability Type: Symlink-following arbitrary file overwrite
Risk Level: HighVulnerable 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_pathTechnical 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
- An attacker supplies a workspace or can modify a workspace that the victim will audit.
- The attacker creates
projects/system/and places a symbolic link namedhygiene-YYYY-MM-DD.md, using the expected execution date. - The symbolic link targets an arbitrary file outside the workspace that is writable by the victim.
- The victim runs ...[truncated 949 chars]
- Remediation
View 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()oros.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()withO_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.
