T09 · Insecure Skill Coding Practices
Warning
- Location
- index.js:48
- Finding
- Path Traversal Through Unvalidated Genre Argument<![CDATA[ ## Vulnerability Details **File Location**: `index.js:48-59` and `index.js:78-80` **Vulnerability Type**: Path traversal and arbitrary file creation **Risk Level**: Medium ### Vulnerable Code ```js async function generateStory(genre) { const prompt = getRandomPrompt(genre); const story = ` **Theme:** ${genre.toUpperCase()} **Prompt:** ${prompt} ...The screen flickered. Code cascaded down like green rain, but the patterns were wrong. They formed faces. Screaming faces made of hexadecimal. "System stable," the console reported. "Soul uploaded." `.trim(); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = path.join(TALES_DIR, `${genre}_${timestamp}.txt`); fs.writeFileSync(filename, story); ``` ```js // CLI const args = process.argv.slice(2); const genre = args.includes('--genre') ? args[args.indexOf('--genre') + 1] : 'ghost'; generateStory(genre).catch(err => { console.error(err); process.exit(1); }); ``` ### Technical Analysis The value following `--genre` is accepted directly from the command line and interpolated into a filesystem path. The code does not restrict the value to the keys in `GENRES`, remove path separators, or verify that the resolved destination remains inside `TALES_DIR`. Although `path.join()` normalizes paths, it does not prevent traversal. A genre containing components such as `../` can cause the resulting filename to resolve outside `../../memory/tales`. The appended timestamp prevents an attacker from choosing the exact final filename, but it does not prevent unauthorized file creation in another writable directory. The story body also incorporates the supplied genre, allowing the attacker to influence part of the written content. ### Attack Path 1. An attacker obtains the ability to invoke the skill or influence its command-line arguments. 2. The attacker supplies a genre containing traversal components, for example: ```bash node index.js --genre ". ...[truncated 1094 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions Enforce an explicit allowlist of supported genres before using the value: ```js const requestedGenre = args.includes('--genre') ? args[args.indexOf('--genre') + 1] : 'ghost'; if (!requestedGenre || !Object.prototype.hasOwnProperty.call(GENRES, requestedGenre)) { throw new Error('Invalid genre. Supported genres: ghost, scifi'); } const genre = requestedGenre; ``` Additionally, resolve the destination and verify that it remains under the intended directory: ```js const safeFilename = `${genre}_${timestamp}.txt`; const filename = path.resolve(TALES_DIR, safeFilename); const talesRoot = `${path.resolve(TALES_DIR)}${path.sep}`; if (!filename.startsWith(talesRoot)) { throw new Error('Invalid output path'); } fs.writeFileSync(filename, story, { encoding: 'utf8', flag: 'wx', mode: 0o600 }); ``` Use a dedicated argument parser or explicitly reject missing option values. Run the skill with a minimally privileged account whose filesystem permissions are limited to the intended output directory. ]]>
