T09 · Insecure Skill Coding Practices
- Location
scripts/code_search.py:75- Finding
Symbolic Link Following Allows Source Disclosure Outside the Indexed Root
- Content
View full analysis
Vulnerability Details
File Location:
scripts/code_search.py:60-77, with sensitive content serialization atscripts/code_search.py:145-149
Vulnerability Type: Improper symbolic-link handling and missing path-boundary validation
Risk Level: MediumThe directory scanner relies on the lexical path returned by
Path.rglob()and does not reject symbolic links or verify that the resolved file remains beneath the requested root directory.Vulnerable code:
python def parse_file(self, path: Path) -> list[CodeFragment]: try: source = path.read_text(encoding="utf-8", errors="ignore") tree = ast.parse(source) except (SyntaxError, UnicodeDecodeError): return [] fragments = [] source_lines = source.splitlines() for node in ast.walk(tree): if isinstance(node, ast.FunctionDef): docstring = ast.get_docstring(node) or "" start = node.lineno end = node.end_lineno or start code = "\n".join(source_lines[start - 1:end]) fragments.append(CodeFragment( file_path=str(path), start_line=start, end_line=end, name=node.name, code=code, docstring=docstring, frag_type="method" if isinstance(getattr(node, 'parent', None), ast.ClassDef) else "function" )) elif isinstance(node, ast.ClassDef): docstring = ast.get_docstring(node) or "" start = node.lineno end = node.end_lineno or start code = "\n".join(source_lines[start - 1:end]) fragments.append(CodeFragment( file_path=str(path), start_line=start, end_line=end, name=node.name, code=code, docstring=docstring, frag_type="class" )) return fragments def parse_directory(self, root: Path, extensions: set[str] = None) -> list[CodeFragment]: extensions = extensi ...[truncated 3209 chars]- Remediation
View remediation
Remediation Suggestions
- Resolve the indexing root once and require every candidate's resolved path to remain beneath it.
- Reject symbolic links explicitly before opening files.
- Perform validation immediately before opening the file to reduce path-substitution opportunities.
- Catch filesystem exceptions such as
OSError,PermissionError, and broken-link errors so unsafe entries do not terminate the scan. - Consider opening files through a directory file descriptor with platform-supported no-follow protections when indexing untrusted repositories and stronger race resistance is required.
- Store only the source fields required for search. If complete source is unnecessary, omit
codeand full docstrings from the persisted index. - Create saved indexes with restrictive permissions and document that they may contain sensitive source material.
Example boundary validation:
python def parse_directory(self, root: Path, extensions: set[str] = None) -> list[CodeFragment]: extensions = extensions or {".py"} root = root.resolve(strict=True) fragments = [] for candidate in root.rglob("*"): try: if candidate.is_symlink() or not candidate.is_file(): continue resolved = candidate.resolve(strict=True) if not resolved.is_relative_to(root): continue relative_parts = resolved.relative_to(root).parts if resolved.suffix not in extensions: continue if any(part.startswith(".") for part in relative_parts): continue fragments.extend(self.parse_file(resolved)) except (OSError, PermissionError): continue return fragmentsWhere supported, supplement this check with no-follow file-opening semantics to mitigate a time-of-check/time-of-use race between validation and reading.
