T09 · Insecure Skill Coding Practices
- Location
scripts/upload-files.py:162- Finding
Folder Upload Scanners Follow Symbolic Links Outside the Selected Directory
- Content
View full analysis
Vulnerability Details
File Location:
scripts/upload-files.py:162-184, 219-220;scripts/sync-folder.ts:224-256;references/folder-sync.md:19-23, 66-73
Vulnerability Type: Symbolic-link path escape leading to unintended file disclosure
Risk Level: MediumRelevant Code
scripts/upload-files.py:162-184:python for file_path in folder.rglob('*'): if file_path.is_dir(): continue # Check ignore patterns relative_path = str(file_path.relative_to(folder)) should_ignore = any(p in relative_path for p in ignore_patterns) if should_ignore: continue # Check extension if extensions and file_path.suffix.lower() not in extensions: continue task = UploadTask( local_path=str(file_path), file_name=file_path.name, parent_entry_id=parent_entry_id, mime_type=get_mime_type(str(file_path)), file_size=file_path.stat().st_size, content_hash=compute_file_hash(str(file_path)) ) tasks.append(task)scripts/upload-files.py:219-220:python with open("{task.local_path}", "rb") as f: requests.put(upload_url, data=f.read(), headers={{"Content-Type": "{task.mime_type}"}})scripts/sync-folder.ts:224-256:typescript const entries = fs.readdirSync(absolutePath, { withFileTypes: true }); for (const entry of entries) { const relativePath = path.join(currentPath, entry.name); if (shouldIgnore(relativePath, config.ignoredPatterns)) { continue; } const entryAbsolutePath = path.join(basePath, relativePath); const stats = fs.statSync(entryAbsolutePath); if (entry.isDirectory()) { files.push({ relativePath, absolutePath: entryAbsolutePath, isDirectory: true, lastModified: stats.mtime }); files.push(...scanLocalDirectory(basePath, config, relativePath)); } else { const ext = ...[truncated 3276 chars]- Remediation
View remediation
Remediation Suggestions
-
Reject symbolic links during scanning:
- In Python, call
file_path.lstat()orfile_path.is_symlink()before anystat, hash, or read operation. - In TypeScript, call
fs.lstatSync(entryAbsolutePath).isSymbolicLink()and skip symbolic links by default.
- In Python, call
-
Resolve and enforce containment:
- Resolve the selected root once with
realpath. - Resolve every candidate file with
realpath. - Require the resolved candidate to be a descendant of the resolved root using path-component-aware comparison.
- Reject candidates that resolve to the root's parent, a sibling directory, or another filesystem location.
- Resolve the selected root once with
-
Revalidate immediately before upload. A file can be replaced with a symlink after scanning, so containment and file-type checks should occur again when opening the file.
-
Where supported, open files using no-follow semantics and verify the opened file descriptor rather than relying solely on pathname checks.
-
Make the upload plan clearly report skipped symlinks and resolved-path violations so users can identify malicious or unexpected directory contents.
-
Add regression tests covering:
- File symlinks to targets outside the upload root.
- Symlinks whose names use allowed extensions.
- Directory symlinks.
- Relative and absolute symlink targets.
- Replacement of a validated file with a symlink between scanning and upload.
-
