T09 · Insecure Skill Coding Practices
Warning
- Location
- lib/artifacts.js:34
- Finding
- Path Traversal Through Unsanitized Run ID Allows Writes Outside the Artifact Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/run.js:15-37, 126`; `lib/artifacts.js:8-26, 30-36`; `lib/generate-image.js:106-125, 232` **Vulnerability Type**: Path traversal leading to unintended directory creation and file overwrite **Risk Level**: Medium ### Vulnerable Code The command-line parser accepts `--run_id` without validation: ```js for (let i = 2; i < argv.length; i += 1) { const key = argv[i]; if (key === '--yes') { args.yes = true; continue; } if (key === '--wait') { args.wait = true; continue; } const value = argv[i + 1]; if (key === '--prompt') args.prompt = value; if (key === '--task_id') args.taskId = value; if (key === '--model') args.model = value; if (key === '--resolution') args.resolution = value; if (key === '--n') args.n = Number(value); if (key === '--aspect_ratio') args.aspectRatio = value; if (key === '--reference_images') args.referenceImages = value.split(',').map((s) => s.trim()); if (key === '--run_id') args.runId = value; if (key === '--timeout_sec') args.timeoutSec = Number(value); if (key === '--poll_interval') args.pollInterval = Number(value); if (key.startsWith('--')) { i += 1; } } ``` The untrusted value is passed into the generation workflow: ```js const result = await runGenerateImage({ prompt: resolvedArgs.prompt, runId: resolvedArgs.runId, skillDir: SKILL_ROOT, model: resolvedArgs.model, resolution: resolvedArgs.resolution, n: resolvedArgs.n, aspectRatio: resolvedArgs.aspectRatio || null, referenceImages: resolvedArgs.referenceImages, timeoutSec: resolvedArgs.timeoutSec, pollInterval: resolvedArgs.pollInterval, }); ``` It is then joined directly to the artifact root: ```js function artifactsRootForSkill(skillDir) { return path.join(skillDir, '.artifacts'); } function artifactsForRun(skillDir, runId) { return new Artifacts(path.join(artifactsRootForSkill(skillDir), runId)); } ``` The resulting path is used for recu ...[truncated 4392 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. **Restrict run IDs to opaque identifiers** Accept only a narrow character set and a reasonable maximum length: ```js function validateRunId(runId) { if (typeof runId !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(runId)) { throw new Error('Invalid run_id'); } return runId; } ``` 2. **Enforce canonical path containment** Resolve both the artifact root and candidate run directory, then verify that the candidate remains a descendant: ```js function artifactsForRun(skillDir, runId) { const safeRunId = validateRunId(runId); const artifactsRoot = path.resolve(skillDir, '.artifacts'); const candidate = path.resolve(artifactsRoot, safeRunId); const relative = path.relative(artifactsRoot, candidate); if ( relative === '' || relative.startsWith(`..${path.sep}`) || relative === '..' || path.isAbsolute(relative) ) { throw new Error('run_id escapes the artifact directory'); } return new Artifacts(candidate); } ``` 3. **Reject path syntax explicitly** Reject absolute paths, `.` and `..` components, forward slashes, backslashes, null bytes, and platform-specific path separators before performing filesystem operations. 4. **Address symlink traversal** Where the execution environment is not fully trusted, check existing path components with `lstat`, reject symbolic links, and use filesystem APIs or deployment permissions that prevent symlink-based redirection. 5. **Reduce overwrite risk** If overwriting existing run artifacts is unnecessary, use exclusive creation such as the `wx` flag or create each run directory with exclusive semantics. Alternatively, generate run IDs internally using a UUID rather than accepting arbitrary caller-provided values. 6. **Apply least-privilege filesystem permissions** Run the Skill under an account that can write only to its designated artifact directory. Thi ...[truncated 346 chars]
