T09 · Insecure Skill Coding Practices
- Location
scripts/idea_capture.py:169- Finding
User-Controlled Idea ID Enables Path Traversal and Arbitrary File Writes
- Content
View full analysis
Vulnerability Details
File Location:
scripts/idea_capture.py:103-105,scripts/idea_capture.py:169-170, andscripts/idea_capture.py:193-194
Vulnerability Type: Path traversal and unrestricted filesystem write
Risk Level: HighThe
--idea-idargument is used as a filesystem path component without validation, normalization, or a check that the resulting path remains inside the intendedideasdirectory.Vulnerable Code
python def write_summary(idea_id: str, title: str, source: str, summary: str, notes: str, open_questions, next_steps, stamp: str): target_dir = SUMMARIES_DIR / idea_id target_dir.mkdir(parents=True, exist_ok=True) path = target_dir / f"{stamp}.md"python if idea is None: idea_id = args.idea_id or slugify(args.title) idea_path = IDEAS_DIR / f"{idea_id}.md" meta = { "id": idea_id, "title": args.title, "status": "active", "tags": tags, "created": today, "updated": updated_at, "source": args.source, } idea_path.write_text( render_idea_doc(meta, args.summary, args.notes, args.open_question, args.next_step, update_entry), encoding="utf-8", )python else: idea_id = idea["id"] idea_path = IDEAS_DIR / f"{idea_id}.md" merged_tags = sorted(set(idea.get("tags", [])) | set(tags))Technical Analysis
Values supplied through
--idea-idbypassslugify()because the explicit value is selected directly:python idea_id = args.idea_id or slugify(args.title)Consequently, an ID can contain parent-directory components such as
../or be an absolute path. The value is subsequently joined toIDEAS_DIRandSUMMARIES_DIRusingpathlib.Path. These joins do not guarantee containment within the intended archive. Parent-directory components are resolved by the operating system during file access, while an ...[truncated 2843 chars]- Remediation
View remediation
Remediation Suggestions
-
Apply strict identifier validation
Permit only a conservative identifier format and reject all other values:
python IDEA_ID_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,127}$") def validate_idea_id(value: str) -> str: if not IDEA_ID_RE.fullmatch(value): raise ValueError( "Invalid idea ID: use lowercase letters, digits, and hyphens only." ) return valueApply this validation to explicit CLI identifiers and IDs loaded from
catalog.json. -
Enforce canonical path containment
Resolve the base directory and destination before every read, write, or directory-creation operation:
python def contained_path(base: Path, relative: str) -> Path: base = base.resolve() destination = (base / relative).resolve() if not destination.is_relative_to(base): raise ValueError("Path escapes the intended storage directory.") return destinationFor example:
python idea_id = validate_idea_id(args.idea_id) if args.idea_id else slugify(args.title) idea_path = contained_path(IDEAS_DIR, f"{idea_id}.md") target_dir = contained_path(SUMMARIES_DIR, idea_id) -
Validate persisted catalog records
Treat
catalog.jsonas untrusted input. Verify its schema and validate every stored ID before using it in a path. Reject or quarantine malformed entries rather than attempting to update them. -
Prevent unintended overwrites in create mode
Use exclusive creation when a new record is expected:
python with idea_path.open("x", encoding="utf-8") as handle: handle.write(content)Alternatively, explicitly detect an existing destination and require confirmation before replacement.
-
Add security regression tests
Test and reject identifiers containing
../,.., path separators, absol ...[truncated 200 chars]
-
