T09 · Insecure Skill Coding Practices
- Location
crabukit/parsers/scripts.py:37- Finding
Scan Boundary Escape Through Symbolic Links and Unbounded File Reads
- Content
View full analysis
Vulnerability Details
File Location:
crabukit/parsers/scripts.py:37-47
Additional Affected Location:crabukit/parsers/skill_md.py:37-42
Vulnerability Type: Improper symbolic-link handling and unrestricted resource consumption
Risk Level: MediumVulnerable Code:
python for file_path in self.scripts_dir.rglob("*"): if not file_path.is_file(): continue # Skip hidden files if file_path.name.startswith('.'): continue script = self._parse_file(file_path) if script: scripts.append(script)python def _parse_file(self, file_path: Path) -> Optional[ScriptFile]: """Parse a single file.""" try: content = file_path.read_text() except (IOError, UnicodeDecodeError): return NoneThe same issue affects
SKILL.mdparsing:python if not self.skill_md_path.exists(): return None try: content = self.skill_md_path.read_text(encoding='utf-8') except (IOError, UnicodeDecodeError) as e: return NoneTechnical Analysis
The scanner processes attacker-controlled directories but uses
Path.is_file()andPath.read_text()without rejecting symbolic links. Both operations follow file symlinks. No resolved-path containment check verifies that a discovered file remains beneath the requested skill directory.An attacker can therefore place a symlink such as
scripts/linked.pythat points to a readable file outside the skill. The scanner reads and analyzes the external target as though it belonged to the skill. If matching code or suspicious content is found, portions may be included in finding snippets and serialized output.Neither script parsing nor
SKILL.mdparsing enforces a maximum file size. A symlink or ordinary file containing a very large input can consequently cause excessive memory use, AST parsing cost, regular-expression processing, and ...[truncated 1670 chars]- Remediation
View remediation
Remediation Suggestions
-
Reject symbolic links before reading:
python if file_path.is_symlink(): continue -
Resolve every candidate path and enforce containment:
python root = self.skill_path.resolve() candidate = file_path.resolve(strict=True) if candidate != root and root not in candidate.parents: continue -
Use
lstat()when inspecting file metadata so the link itself is examined rather than its target. -
Apply the same checks to
SKILL.md, thescriptsdirectory, hidden-file checks, file-type checks, and permission analysis. -
Enforce conservative per-file and total-scan byte limits before reading. Read incrementally where possible rather than loading an entire file into memory.
-
Catch resource-related failures and report skipped files without terminating the complete scan.
-
Add regression tests covering:
- A script symlink targeting a file outside the skill.
- A symlinked
SKILL.md. - Broken and recursive symlinks.
- Oversized Python, shell, and Markdown files.
- Verification that no content outside the scan root appears in output.
-
