T09 · Insecure Skill Coding Practices
- Location
scripts/research.py:153- Finding
Path Traversal Permits Out-of-Scope Markdown File Access
- Content
View full analysis
Vulnerability Details
File Location:
scripts/research.py, lines 153-162; related unsafe path construction at lines 61-77
Vulnerability Type: Path traversal and insufficient input validation
Risk Level: MediumVulnerable Code
python def cmd_cite(args): report_path = RESEARCH_DIR / f"{args.slug}.md" if not report_path.exists(): print(f"Report file not found: {report_path}") sys.exit(1) content = report_path.read_text(encoding="utf-8") # Extract citation lines (lines starting with [n]) citations = re.findall(r"^\[(\d+)\]\s+(.+)$", content, re.MULTILINE)The same unvalidated path construction is used by the
addcommand:python def cmd_add(args): index = load_index() now = datetime.now(timezone.utc).isoformat() slug = args.slug report_path = RESEARCH_DIR / f"{slug}.md" entry = { "title": args.title, "slug": slug, "depth": args.depth or "standard", "confidence": args.confidence or "MEDIUM", "tags": [t.strip() for t in args.tags.split(",")] if args.tags else [], "created": now, "updated": now, "file": str(report_path), "sources_count": 0, "related": [], } # Count sources if report file already exists if report_path.exists(): content = report_path.read_text(encoding="utf-8") sources = re.findall(r"^\[(\d+)\]", content, re.MULTILINE) entry["sources_count"] = len(sources)Technical Analysis
The
slugargument is accepted directly from the command line and interpolated into a filesystem path without validation. The code assumes that joining the supplied value withRESEARCH_DIRconfines access to~/research, butpathlibdoes not enforce that boundary.A slug containing parent-directory components such as
../can escape the research directory. An absolute slug can ...[truncated 2032 chars]- Remediation
View remediation
Remediation Suggestions
-
Enforce a strict slug allowlist before performing any filesystem operation. For example:
python SLUG_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]*$") def validate_slug(slug: str) -> str: if not SLUG_PATTERN.fullmatch(slug): raise ValueError( "Invalid slug: use only letters, digits, underscores, and hyphens" ) return slug -
Resolve and verify the final path defensively:
python def report_path_for(slug: str) -> Path: slug = validate_slug(slug) base = RESEARCH_DIR.resolve() candidate = (base / f"{slug}.md").resolve() if candidate.parent != base: raise ValueError("Report path escapes the research directory") return candidate -
Use the centralized helper in both
cmd_citeandcmd_add, as well as in any future command that derives a path from a slug. -
Explicitly reject absolute paths, path separators,
.components, and..components even if an allowlist is introduced. -
Handle invalid paths and decoding failures with controlled error messages rather than exposing resolved external paths or Python tracebacks.
-
Consider limiting input file size before calling
read_text()to reduce denial-of-service risk from unexpectedly large files. -
Add regression tests covering:
- Valid alphanumeric, hyphenated, and underscored slugs.
- Parent traversal using
../. - Absolute paths.
- Nested path separators.
- Symbolic-link boundary cases.
- Files outside
RESEARCH_DIR.
-
