T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/port_skill.py:332
- Finding
- Source Symlink Allows Files Outside the Skill Directory to Be Copied## Vulnerability Details **File Location**: `scripts/port_skill.py`, lines 332–378 **Vulnerability Type**: Source-directory boundary bypass through symbolic links **Risk Level**: Medium ### Vulnerable Code ```python for p in sorted(src.rglob("*")): if p.is_dir(): continue rel = p.relative_to(src) is_text = p.suffix.lower() in TEXT_EXTS if is_text: orig = p.read_text(encoding="utf-8", errors="replace") if _is_tool_catalog(str(rel), orig): warnings.append(f"Tool mapping document: {rel} skipped auto-replacement; must be fully rewritten as the target platform's tool table.") files_copied += 1 if not dry_run: dst = dst_root / rel dst.parent.mkdir(parents=True, exist_ok=True) dst.write_text(orig, encoding="utf-8") continue text = orig if rel.name == "SKILL.md": if fix_name and name_mismatch: text = _fix_name(text, skill_name) text = normalize_frontmatter(text, target) file_flags = [] text, n = transform_text(text, target, file_flags, str(rel)) files_copied += 1 if n: files_changed += 1 total_changes += n if strip_output_directives: text_after_strip, n_strip = _strip_output_directives(text) if n_strip: file_flags = [f for f in file_flags if not _is_output_directive_flag(f)] file_flags.append(f"{rel}: (auto-removed {n_strip} output directive(s))") directives_removed += n_strip text = text_after_strip flags.extend(file_flags) if text != orig: diffs.append((str(rel), orig, text)) if not dry_run: dst = dst_root / rel dst.parent.mkdir(parents=True, exist_ok=True) dst.write_text(text, encoding="utf-8") else: files_copied += 1 if not dry_run: ...[truncated 2472 chars]
- Remediation
- ## Remediation Suggestions - Reject symbolic links before processing: ```python if p.is_symlink(): raise ValueError(f"symbolic links are not allowed in source skills: {p}") ``` - Resolve every candidate and enforce containment beneath the canonical source directory before reading or copying: ```python src_root = src.resolve() for p in sorted(src.rglob("*")): if p.is_symlink(): raise ValueError(f"symbolic links are not allowed: {p}") resolved = p.resolve(strict=True) if resolved != src_root and src_root not in resolved.parents: raise ValueError(f"source entry escapes source directory: {p}") if resolved.is_dir(): continue ``` - Prefer file-opening mechanisms with no-follow semantics, where supported, to reduce time-of-check/time-of-use symlink races. - Apply the same containment policy to every source entry regardless of whether it is treated as text or copied as binary data. - Fail closed when encountering broken links, special files, sockets, FIFOs, or device nodes; accept only regular files and directories. - Add regression tests covering text and binary symlinks to external files, nested symlinks, broken symlinks, and links changed during processing.
