T09 · Insecure Skill Coding Practices
- Location
scripts/run.py:174- Finding
Unrestricted Output Paths Permit Writes Outside the Workspace
- Content
View full analysis
Vulnerability Details
File Location:
scripts/run.py, lines 174–183; write operations at lines 288 and 307
Vulnerability Type: Path traversal and arbitrary file overwrite
Risk Level: MediumVulnerable Code
python outputs = parse_semicolon_list(args.outputs) or ["outline/tables_appendix.md", "output/TABLES_APPENDIX_REPORT.md"] out_rel = next((x for x in outputs if x.endswith("tables_appendix.md")), "outline/tables_appendix.md") report_rel = next((x for x in outputs if x.endswith("TABLES_APPENDIX_REPORT.md")), "output/TABLES_APPENDIX_REPORT.md") out_path = workspace / out_rel report_path = workspace / report_rel ensure_dir(out_path.parent) ensure_dir(report_path.parent)The resulting paths are later used as write destinations:
python atomic_write_text(out_path, text)python atomic_write_text(report_path, "\n".join(rep_lines).rstrip() + "\n")Technical Analysis
The
--outputscommand-line argument is externally controlled and parsed into destination path strings. The code joins those strings toworkspacewithout rejecting absolute paths, resolving traversal components, or verifying that the resulting destinations remain inside the workspace.In Python, joining a
Pathwith an absolute operand discards the preceding workspace path. Relative values containing../can likewise traverse outside the workspace. The suffix checks only require paths to end withtables_appendix.mdorTABLES_APPENDIX_REPORT.md; they do not provide filesystem containment.The destinations are passed to
atomic_write_text, which creates the parent directory when possible and atomically replaces the target. Consequently, a caller who can control the command-line arguments can overwrite files in any location writable by the process. This exceeds the Skill's declared need to write onlyoutline/tables_appendix.mdandoutput/TABLES_APPENDIX_REPORT.mdwithin the selected workspace.Attack Path
- The attacker invokes the S ...[truncated 1583 chars]
- Remediation
View remediation
Remediation Suggestions
Constrain every output destination to the resolved workspace before creating directories or writing files:
python workspace = Path(args.workspace).resolve() def resolve_workspace_output(rel: str) -> Path: supplied = Path(rel) if supplied.is_absolute(): raise ValueError(f"Absolute output paths are not allowed: {rel}") candidate = (workspace / supplied).resolve() try: candidate.relative_to(workspace) except ValueError as exc: raise ValueError(f"Output path escapes the workspace: {rel}") from exc return candidate out_path = resolve_workspace_output(out_rel) report_path = resolve_workspace_output(report_rel)Additional hardening should include:
- Prefer a strict allowlist containing only:
outline/tables_appendix.mdoutput/TABLES_APPENDIX_REPORT.md
- Reject empty paths, absolute paths, and any path containing traversal components before filesystem access.
- Perform containment validation before calling
ensure_dir. - Consider rejecting symlinked destination parents or verifying the resolved parent again immediately before replacement where hostile users can modify the workspace concurrently.
- Add tests for:
../traversal.- Nested traversal.
- Absolute Unix and Windows paths.
- Symlink-based workspace escape.
- Valid workspace-relative output paths.
- Apply the same containment validation to output paths accepted by the bundled executor and quality-gate interfaces.
- Prefer a strict allowlist containing only:
