T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/brand.mjs:49
- Finding
- Arbitrary Local File Disclosure Through Unvalidated Brand Reference Paths<![CDATA[ ## Vulnerability Details **File Location**: `scripts/brand.mjs:49-51`, `scripts/gen.mjs:129-130`, and provider upload sinks in `scripts/lib/providers.mjs:33-43, 149-151, 203-209, 234-239, 263-268, 298-301` **Vulnerability Type**: Arbitrary local file read and disclosure to an external generation provider **Risk Level**: High ### Vulnerable Code `scripts/brand.mjs:49-51` accepts any existing path from `model.reference`: ```js if (m.reference) { if (existsSync(m.reference)) images.push(m.reference) else parts.push(`(模特参考图 ${m.reference} 找不到,已跳过)`) } ``` `scripts/gen.mjs:129-130` automatically adds that path to the files sent to the selected provider: ```js if (b.append) o.prompt = `${o.prompt}\n${b.append}` if (b.images.length) o.images = [...o.images, ...b.images] ``` For example, `scripts/lib/providers.mjs:33-43` reads local paths without restricting them to image assets: ```js async function asBase64(p) { if (isUrl(p)) { const r = await fetch(p) if (!r.ok) throw new Error(`拉取参考图失败 ${r.status}: ${p}`) return Buffer.from(await r.arrayBuffer()).toString('base64') } return (await readFile(p)).toString('base64') } const asDataUri = async (p) => isUrl(p) ? p : `data:${mimeOf(p)};base64,${await asBase64(p)}` ``` The resulting data is transmitted by provider adapters. For example, the Gemini adapter at `scripts/lib/providers.mjs:203-209` embeds it into an external API request: ```js for (const p of req.images || []) { parts.push({ inline_data: { mime_type: mimeOf(p), data: await asBase64(p) } }) } const j = await postJson( `https://generativelanguage.googleapis.com/v1beta/models/${gemini.model()}:generateContent`, { contents: [{ parts }] }, { 'x-goog-api-key': key }, req.timeoutMs, ) ``` Equivalent local-file upload behavior exists in the OpenAI, fal, Replicate, Ark, and dLazy execution paths. ### Technical Analysis The `--brand` feature is intended to append brand constraints and optionally include a model refe ...[truncated 3338 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. **Constrain reference paths to an approved directory** - Resolve relative references against the directory containing the brand file. - Canonicalize both the approved root and candidate path with `realpath()`. - Reject candidates whose canonical path is outside the approved root. 2. **Reject dangerous file types** - Use `lstat()` and reject symbolic links, devices, directories, sockets, and other non-regular files. - Permit only an explicit image extension allowlist such as `.jpg`, `.jpeg`, `.png`, and `.webp`. - Validate the file's actual magic bytes rather than trusting its extension. 3. **Apply resource limits** - Enforce a conservative maximum file size before reading or uploading. - Optionally validate image dimensions and decode the image before submission. 4. **Require informed user approval** - Display the canonical path of every automatically added brand asset. - Require explicit confirmation before uploading files not directly supplied through `--images`. - Clearly identify the selected provider and external destination. 5. **Fail closed** - Do not silently skip or accept malformed references. - Reject absolute paths and traversal attempts unless explicitly authorized. - Avoid defaulting unknown files to `image/jpeg`. 6. **Harden all provider adapters** - Centralize local input validation before any provider adapter receives `req.images`. - Ensure the dLazy CLI path receives the same validation as HTTP providers. - Add tests covering absolute paths, `../` traversal, symbolic-link escapes, non-image files, oversized files, and valid in-directory images. A secure resolution pattern should conceptually follow: ```js const brandDir = await realpath(path.dirname(brandFile)) const candidate = await realpath(path.resolve(brandDir, m.reference)) const relative = path.relative(brandDir, candidate) if (relative.startsWith('..') || path.isAbsolute(relative)) { throw ...[truncated 331 chars]
