T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/publish-node.mjs:130
- Finding
- Arbitrary Local File Read and Network Disclosure Through Article Image Paths<![CDATA[ ## Vulnerability Details **File Location**: `scripts/publish-node.mjs:130-142`; `scripts/publish-python.py:199-209` **Vulnerability Type**: Unrestricted local file access and upload **Risk Level**: High ### Complete Code Snippets Node.js implementation: ```javascript async function processContentImages(config, accessToken, html, baseDir) { const srcMatches = [...html.matchAll(/<img[^>]*src=["']([^"']+)["'][^>]*>/gi)]; let processed = html; for (const match of srcMatches) { const src = match[1]; if (/^https?:\/\//i.test(src) || src.startsWith("data:")) { continue; } const imagePath = path.isAbsolute(src) ? src : path.resolve(baseDir, src); const uploaded = await uploadImage(config, accessToken, imagePath, false); if (uploaded.url) { processed = processed.replaceAll(`src="${src}"`, `src="${uploaded.url}"`); processed = processed.replaceAll(`src='${src}'`, `src='${uploaded.url}'`); } } return processed; } ``` Python implementation: ```python async def process_content_images(self, access_token: str, content: str, content_dir: Path) -> str: processed = content matches = re.findall(r'<img[^>]*src=["\']([^"\']+)["\'][^>]*>', content, flags=re.IGNORECASE) for src in matches: if src.startswith(("http://", "https://", "data:")): continue image_path = Path(src) if not image_path.is_absolute(): image_path = (content_dir / src).resolve() uploaded = await self.upload_image(access_token, str(image_path), is_thumb=False) if uploaded.get("url"): processed = processed.replace(f'src="{src}"', f'src="{uploaded["url"]}"') processed = processed.replace(f"src='{src}'", f"src='{uploaded['url']}'") return processed ``` ### Technical Analysis Both publishers treat every non-HTTP and non-`data:` image source as a local filesystem path. Absolute paths are accepted directly, while relative paths are resolved withou ...[truncated 1819 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Reject absolute image paths from article HTML. 2. Resolve each relative path against a single explicitly approved content root. 3. Verify containment after canonicalization: ```javascript const root = await fs.realpath(baseDir); const candidate = await fs.realpath(path.resolve(root, src)); const relative = path.relative(root, candidate); if (relative.startsWith("..") || path.isAbsolute(relative)) { throw new Error("Image path escapes the approved content directory"); } ``` 4. Apply the equivalent Python check with `Path.resolve()` and `Path.is_relative_to()`: ```python root = content_dir.resolve(strict=True) candidate = (root / src).resolve(strict=True) if not candidate.is_relative_to(root): raise ValueError("Image path escapes the approved content directory") ``` 5. Reject symbolic links that resolve outside the approved root. 6. Require an allowlisted extension and validate the file's actual magic bytes as JPEG, PNG, GIF, or another explicitly supported image format. 7. Impose per-file and aggregate upload size limits. 8. Consider requiring explicit image-file arguments rather than automatically reading every local path embedded in HTML. ]]>
