T09 · Insecure Skill Coding Practices
- Location
scripts/run.py:72- Finding
Unrestricted workspace path overrides allow arbitrary file reads and replacement
- Content
View full analysis
Vulnerability Details
File Location:
scripts/run.py:72-89,scripts/run.py:358, andtooling/common.py:25-39
Vulnerability Type: Workspace path traversal and insufficient filesystem boundary validation
Risk Level: MediumVulnerable Code
scripts/run.py:72-89:python inputs = parse_semicolon_list(args.inputs) or [ "output/DRAFT.md", "outline/outline.yml", "outline/writer_context_packs.jsonl", "citations/ref.bib", ] outputs = parse_semicolon_list(args.outputs) or ["output/CITATION_BUDGET_REPORT.md"] draft_rel = next((p for p in inputs if p.endswith("DRAFT.md")), "output/DRAFT.md") outline_rel = next((p for p in inputs if p.endswith("outline.yml")), "outline/outline.yml") packs_rel = next((p for p in inputs if p.endswith("writer_context_packs.jsonl")), "outline/writer_context_packs.jsonl") bib_rel = next((p for p in inputs if p.endswith("ref.bib")), "citations/ref.bib") out_rel = outputs[0] if outputs else "output/CITATION_BUDGET_REPORT.md" out_path = workspace / out_rel draft_path = workspace / draft_rel outline_path = workspace / outline_rel packs_path = workspace / packs_rel bib_path = workspace / bib_relscripts/run.py:358:python atomic_write_text(out_path, "\n".join(lines).rstrip() + "\n")tooling/common.py:25-39:python def ensure_dir(path: Path) -> None: path.mkdir(parents=True, exist_ok=True) def atomic_write_text(path: Path, content: str) -> None: ensure_dir(path.parent) fd, tmp_path = tempfile.mkstemp(prefix=path.name, dir=str(path.parent)) try: with os.fdopen(fd, "w", encoding="utf-8") as handle: handle.write(content) os.replace(tmp_path, path) except Exception: try: os.unlink(tmp_path) except OSError: pass raiseTechnical Analysis
The
--inputsand--outputsarguments are parsed ...[truncated 2812 chars]- Remediation
View remediation
Remediation Suggestions
-
Resolve and validate the workspace once:
python workspace = Path(args.workspace).resolve() -
Introduce a single path-confinement helper for every input and output:
python def workspace_path(workspace: Path, value: str) -> Path: rel = Path(value) if rel.is_absolute(): raise ValueError(f"Absolute paths are not allowed: {value}") candidate = (workspace / rel).resolve() try: candidate.relative_to(workspace) except ValueError as exc: raise ValueError(f"Path escapes workspace: {value}") from exc return candidate -
Use this helper for
draft_path,outline_path,packs_path,bib_path, andout_pathbefore any existence check, read, directory creation, or write. -
Restrict output overrides to the documented
output/directory. If arbitrary output selection is unnecessary, remove--outputsentirely and always write tooutput/CITATION_BUDGET_REPORT.md. -
Validate path values obtained from
UNITS.csvbefore forwarding them to the subprocess. Defense in depth should be implemented in both the executor and the Skill entry point. -
Account for symlink-based escapes. Validate resolved paths immediately before access and avoid operating in attacker-controlled parent directories where symlinks can be changed concurrently.
-
Add regression tests covering absolute paths,
../traversal, symlinked directories, malformed unit output fields, and valid workspace-local overrides.
-
