T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/video_gen.js:206
- Finding
- Unrestricted Local File Read and Upload Through Image Parameters<![CDATA[ ## Vulnerability Details **File Location**: `scripts/video_gen.js`, lines 206–283 **Vulnerability Type**: Arbitrary local-file disclosure to a third-party service **Risk Level**: Medium ### Vulnerable Code ```javascript async function uploadFileToPublicUrl(inputPath, apiKey) { const resolvedPath = normalizeLocalFilePath(inputPath); if (!resolvedPath) { throw new Error(`Invalid local image path: ${inputPath}`); } const filePath = resolvedPath instanceof URL ? fileURLToPath(resolvedPath) : path.resolve(resolvedPath); let stat; try { stat = await fs.stat(filePath); } catch { throw new Error(`Local image file not found: ${inputPath}`); } if (!stat.isFile()) { throw new Error(`Local image path is not a file: ${inputPath}`); } const fileBuffer = await fs.readFile(filePath); const fileName = path.basename(filePath); const mimeType = inferMimeType(filePath); const form = new FormData(); form.append('batch_no', makeUploadBatchNo()); form.append('fixed', 'false'); form.append('file', new Blob([fileBuffer], { type: mimeType }), fileName); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 60_000); let res; try { res = await fetch(MODELS_BASE_URL + UPLOAD_API_PATH, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, }, body: form, signal: controller.signal, }); } catch (err) { clearTimeout(timer); if (err.name === 'AbortError') { throw new Error(`Upload timeout: ${filePath}`); } throw err; } clearTimeout(timer); let data; try { data = await res.json(); } catch { throw new Error(`Upload failed with non-JSON response (HTTP ${res.status}).`); } const wrapped = { httpStatus: res.status, ...data }; if (!isApiSuccess(wrapped)) { const apiErr = formatApiError(wrapped); throw new Error(apiErr.errorMessage || `Upload failed (HTTP ${res.status}).`); } ...[truncated 3712 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. **Disable local-file handling by default.** Accept only public HTTPS URLs unless the caller supplies an explicit option such as `--allow-local-upload`. 2. **Require file-specific approval.** Before reading a file, display its canonical path, size, and destination host and require affirmative confirmation. For noninteractive use, require a separate explicit flag for each approved path. 3. **Constrain accessible paths.** Resolve the canonical path with `fs.realpath()` and ensure it remains inside a caller-designated upload directory. 4. **Validate actual image content.** Allowlist supported extensions and verify file signatures or decode the image before upload. Reject `application/octet-stream`. 5. **Apply size limits.** Check `stat.size` before reading and reject files above a conservative limit. Prefer bounded streaming where the API supports it. 6. **Address link traversal.** Use `lstat()` and reject symbolic links, or canonicalize the target and reapply the approved-directory constraint. 7. **Separate capabilities.** Keep URL-based generation independent from local-file upload so normal generation runs do not require filesystem-read behavior. 8. **Preserve documentation controls.** Retain the existing consent warnings in `SKILL.md` and the API guide, but treat them as supplementary safeguards rather than substitutes for runtime enforcement. ]]>
