T09 · Insecure Skill Coding Practices
- Location
runtime.py:493- Finding
Unsanitized Report Slug Enables Path Traversal and File Overwrite
- Content
View full analysis
Vulnerability Details
File Location:
runtime.py:493-503, with file-write sinks atruntime.py:26-28,runtime.py:315-329, andruntime.py:660-674
Vulnerability Type: Path traversal and unintended file overwrite
Risk Level: HighVulnerable Code
The report slug is read from input-controlled JSON and used directly to construct the manifest path:
python def run_feishu_bitable_sync(input_path: Path, *, workspace_root: Path) -> dict[str, Any]: raw_path, payload = load_raw_payload(input_path) request = payload.get("request") or {} articles = payload.get("articles") or [] topic = clean_text(request.get("topic") or raw_path.stem) report_slug = clean_text(request.get("slug") or raw_path.stem) app_token = require_env("FEISHU_BITABLE_APP_TOKEN") table_id = require_env("FEISHU_BITABLE_TABLE_ID") auth_mode = resolve_auth_mode() manifest_path = workspace_root / "content-production" / "published" / f"{datetime.now().strftime('%Y%m%d')}-{report_slug}-feishu-sync.md"The same unvalidated slug is used to construct the CSV fallback path:
python def export_csv_fallback( *, workspace_root: Path, report_slug: str, topic: str, articles: list[dict[str, Any]], ) -> Path: csv_path = workspace_root / "content-production" / "published" / f"{datetime.now().strftime('%Y%m%d')}-{report_slug}-feishu-import.csv" csv_path.parent.mkdir(parents=True, exist_ok=True) exported_at = datetime.now().isoformat(timespec="seconds") with csv_path.open("w", encoding="utf-8-sig", newline="") as handle: writer = csv.DictWriter(handle, fieldnames=csv_headers()) writer.writeheader() for article in articles: source_url = clean_text(article.get("source_url")) if not source_url: continue writer.writerow(build_csv_row(article, topic=topic, report_slug=report_slug, exported_at=exported_at)) return csv_pathThe generic write ...[truncated 3911 chars]
- Remediation
View remediation
Remediation Suggestions
- Treat the slug strictly as an identifier rather than a filesystem path. Enforce a conservative allowlist before constructing output paths:
python SAFE_SLUG = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$") def validate_slug(value: str) -> str: slug = clean_text(value) if not SAFE_SLUG.fullmatch(slug): raise ValueError( "Invalid report slug: only letters, digits, underscores, and hyphens are allowed." ) return slug- Apply validation immediately after reading the input:
python report_slug = validate_slug(request.get("slug") or raw_path.stem)- Add defense-in-depth path confinement for every generated output:
python published_root = ( workspace_root / "content-production" / "published" ).resolve() manifest_path = ( published_root / f"{datetime.now().strftime('%Y%m%d')}-{report_slug}-feishu-sync.md" ).resolve() if published_root not in manifest_path.parents: raise ValueError("Generated manifest path escapes the publication directory.")Perform the equivalent check for
csv_path.-
Explicitly reject absolute paths,
..,/, backslashes, drive prefixes, and platform-specific separator variants even if an allowlist is also used. -
Avoid silent overwrites where practical. Use exclusive creation, a securely generated unique filename, or an explicit overwrite policy when replacing an existing report is not required.
-
Add tests covering Unix and Windows traversal forms, repeated traversal components, absolute paths, mixed separators, encoded separators, excessively long slugs, and symlinked output directories.
-
Consider opening files relative to a trusted directory descriptor with operating-system protections against symlink traversal if the output directory may be writable by untrusted users.
