T09 · Insecure Skill Coding Practices
- Location
scripts/scaffold_project.py:51- Finding
Unrestricted and Destructive Project File Overwrites
- Content
View full analysis
Vulnerability Details
File Location:
scripts/scaffold_project.py, lines 51–57 and 127–143
Vulnerability Type: Unrestricted filesystem writes and symlink-following overwrite
Risk Level: MediumVulnerable Code
python parser.add_argument("--out-dir", default="novel") ... slug = slugify(args.title) root = Path(args.out_dir).expanduser().resolve() / slug chapters = root / "chapters" state_dir = root / "state" root.mkdir(parents=True, exist_ok=True) chapters.mkdir(parents=True, exist_ok=True) state_dir.mkdir(parents=True, exist_ok=True)python (root / "project.json").write_text(json.dumps(project, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") (root / "worldbuilding.md").write_text("# Worldbuilding\n\n", encoding="utf-8") (root / "characters.md").write_text("# Characters\n\n", encoding="utf-8") (root / "outline.md").write_text("# Outline\n\n", encoding="utf-8") (root / "style.md").write_text("# Style\n\n", encoding="utf-8") (root / "memory.md").write_text("# Memory\n\n", encoding="utf-8") (state_dir / "current.json").write_text(json.dumps(current_state, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") (chapters / f"{chapter_number:04d}.md").write_text(f"# Chapter {chapter_number}\n\n", encoding="utf-8") if args.resume_from: (root / "resume.md").write_text( "# Resume Notes\n\n" "## Recovery checkpoint\n" f"- resumeMode: {args.resume_mode}\n" f"- resumeFrom: {args.resume_from}\n" f"- latestChapterIndex: {args.latest_chapter_index}\n" f"- nextChapter: {chapter_number}\n" "- lastStableSentenceOrBeat: \n" "- knownCanonFacts: []\n" "- unknownsThatMustNotBeInvented: []\n" f"- partialDraftSource: {args.partial_draft}\n" "- nextAction: continue recovery\n" "- userConfirmationRequired: yes\n", encoding="utf-8", )Technical Anal
...[truncated 2394 chars]
- Remediation
View remediation
Remediation Suggestions
-
Require a configured workspace root and ensure the resolved project path remains beneath it:
python workspace = Path(args.workspace).expanduser().resolve() root = (workspace / slug).resolve() if not root.is_relative_to(workspace): raise SystemExit("project path escapes the approved workspace") -
Refuse to scaffold into an existing non-empty directory unless the caller supplies an explicit
--forceor--overwriteoption. -
Before writing, inspect every destination component and target with
lstat()and reject symbolic links. -
Use exclusive file creation for new projects, such as mode
"x", rather than unconditional truncation. -
For intentional updates, write to a temporary file in the same directory, flush and synchronize it, and atomically replace the destination.
-
Back up existing project files before an explicitly authorized overwrite.
-
Run the script under a minimally privileged account whose writable scope is limited to the novel workspace.
-
