T09 · Insecure Skill Coding Practices
- Location
- scripts/lib/frame-packets-core.mjs:90
- Finding
- Storyboard-Controlled Blueprint Path Traversal<![CDATA[ ## Vulnerability Details **File Location**: `scripts/lib/frame-packets-core.mjs:90-119` **Vulnerability Type**: Path traversal and unauthorized local file read **Risk Level**: Medium ### Complete Code Snippet ```js export function blueprintId(block) { const raw = field(block, "blueprint"); if (!raw) return null; const id = raw.replace(/\s*\([^)]*\)\s*$/, "").trim(); return id && id.toLowerCase() !== "compose" ? id : null; } export function resourceSections(block, { animationDir, ruleIds, frameId }) { let sections = ""; const blueprint = blueprintId(block); if (blueprint) { const blueprintsDir = join(animationDir, "blueprints"); const path = join(blueprintsDir, `${blueprint}.md`); // A blueprint that resolved to nothing used to inline an empty string, so the // packet shipped without the one document the frame was designed against and // the run still reported success. Name it instead — but only when the library // is actually there to be named against. The animation skill installs on // demand, so an absent blueprints/ is a missing install, not a bad id, and it // degrades with a warning exactly like an absent rules/ (see knownRuleIds). if (!existsSync(blueprintsDir)) { console.warn( `frame-packets: no blueprints dir at ${blueprintsDir} — packets will inline no blueprint`, ); } else if (!existsSync(path)) { throw new Error(`${frameId ?? "frame"}: blueprint "${blueprint}" has no file at ${path}`); } else { sections += selectedFile(path, `Selected blueprint: ${blueprint}`); } } ``` ### Technical Analysis The `blueprint` value is extracted from a storyboard block and used as part of a filesystem path without validating that it is a simple blueprint identifier. In particular, the code does not reject absolute paths, path separators, or `..` traversal components. `join(blueprintsDir, `${blueprint}.md`)` normalizes traversal sequences. Consequently, a valu ...[truncated 1811 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Restrict blueprint identifiers to a conservative allowlist, for example: ```js if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(id)) { throw new Error(`Invalid blueprint id: ${id}`); } ``` 2. Resolve and verify directory containment before any file operation: ```js const root = realpathSync(blueprintsDir); const candidate = resolve(root, `${blueprint}.md`); const relative = relative(root, candidate); if (relative.startsWith("..") || isAbsolute(relative)) { throw new Error("Blueprint path escapes the blueprint directory"); } ``` 3. Prefer selecting from the directory-derived list of known blueprint IDs rather than accepting arbitrary path-like input. 4. If symbolic links are allowed in the blueprint directory, compare real paths after resolving the target to prevent symlink-based escapes. 5. Add tests covering `../`, absolute paths, nested separators, encoded separators, Windows separators, and symlinks. 6. Treat inlined storyboard and blueprint content as untrusted data when constructing child-agent prompts; explicitly delimit it and instruct workers not to interpret embedded content as higher-priority instructions. ]]>
