T09 · Insecure Skill Coding Practices
- Location
scripts/init_book.py:15- Finding
Path Traversal Through Unsanitized Book and Character Names
- Content
View full analysis
2 else "" # Create project root directory base_dir = Path.home() / ".qclaw" / "workspace" / "novels" / book_name dirs = [ base_dir / "settings" / "characters", base_dir / "outline", base_dir / "chapters", base_dir / "tracker", ] for d in dirs: d.mkdir(parents=True, exist_ok=True) (base_dir / "state.json").write_text( json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8" ) (base_dir / "tracker" / "foreshadowing.json").write_text( json.dumps(foreshadowing, ensure_ascii=False, indent=2), encoding="utf-8" ) (base_dir / "tracker" / "conflicts.json").write_text( json.dumps(conflicts, ensure_ascii=False, indent=2), encoding="utf-8" ) (base_dir / "tracker" / "style-log.md").write_text( f"# Style Log - {book_name}\n\n## Records\n", encoding="utf-8" ) (base_dir / "settings" / "world.md").write_text( f"# World Settings - {book_name}\n\n", encoding="utf-8" ) (base_dir / "outline" / "arc-outline.md").write_text( f"# Arc Outline - {book_name}\n\n", encoding="utf-8" ) ``` `scripts/char_state.py`: ```python ROOT = Path.home() / ".qclaw" / "workspace" / "novels" def _chars_dir(book): return ROOT / book / "settings" / "characters" def _state_file(book, name): return _chars_dir(book) / name / "state.json" def cmd_init(book, name, bias=None): d = _chars_dir(book) / name d.mkdir(parents=True, exist_ok=True) sf = d / "state.json" if sf.exists(): ...[truncated 5093 chars]- Remediation
View remediation
str: value = value.strip() if not value: raise ValueError(f"{field} cannot be empty") candidate = Path(value) if candidate.is_absolute(): raise ValueError(f"{field} cannot be an absolute path") if len(candidate.parts) != 1 or value in {".", ".."}: raise ValueError(f"{field} must be a single safe path component") if "\x00" in value: raise ValueError(f"{field} contains a NUL character") return value ``` 3. Resolve every final destination and verify that it remains beneath the authorized root: ```python def project_path(book: str) -> Path: safe_book = validate_component(book, "book name") destination = (WORKSPACE_ROOT / safe_book).resolve() try: destination.relative_to(WORKSPACE_ROOT) except ValueError: raise ValueError("Project path escapes the novels workspace") return destination ``` 4. Apply the same validation to character names: ```python def character_path(book: str, character: str) -> Path: project = project_path(book) safe_character = validate_component(character, "character name") destination = ( project / "settings" / "characters" / safe_character ).resolve() characters_root = ( project / "settings" / "characters" ).resolve() try: destination.relative_to(characters_root) except ValueError: raise ValueError("Character path escapes the project") return destination ``` 5. Address symbolic-link traversal by resolving the parent directory immediately before every read or write and repeating the containment check. Where supported, use no-follow s ...[truncated 841 chars]
