T09 · Insecure Skill Coding Practices
Warning
- Location
- audio-forge.js:175
- Finding
- Predictable Shared Temporary File Enables Symlink-Based File Overwrite<![CDATA[ ## Vulnerability Details **File Location**: `audio-forge.js`, lines 175-190 **Vulnerability Type**: Insecure temporary-file handling **Risk Level**: Medium ### Vulnerable Code ```js function cmdMerge(files, output) { const listFile = path.join(process.env.TEMP || "/tmp", "xza_merge_list.txt"); console.log(`\n 音频合并`); console.log(` 文件数量: ${files.length}`); const content = files.map(f => `file '${f.replace(/\\/g, "/")}'`).join("\n"); fs.writeFileSync(listFile, content, "utf8"); const args = [ "-f", "concat", "-safe", "0", "-i", `"${listFile}"`, "-c", "copy", `"${output}"` ]; runFFmpeg(args); fs.unlinkSync(listFile); console.log(`\n 合并完成: ${output}`); } ``` ### Technical Analysis Every merge operation uses the same predictable temporary path: `/tmp/xza_merge_list.txt` on Unix-like systems, or the equivalent path beneath the directory specified by `TEMP`. `fs.writeFileSync()` follows symbolic links and opens an existing file for truncation by default. It does not use exclusive creation, verify that the destination is a regular file, or ensure that the file was created inside a private temporary directory. Consequently, another local process can prepare the predictable path as a symbolic link to a file writable by the user running this Skill. The shared filename also creates a race between concurrent Skill invocations. One process can replace, modify, or delete another process's FFmpeg concat list. This is particularly problematic because `runFFmpeg()` starts FFmpeg asynchronously and returns immediately, after which `cmdMerge()` immediately deletes the list file. Although this latter behavior is primarily a reliability defect, it increases the opportunity for race conditions and input manipulation. ### Attack Path 1. The attacker has local access to the same system and can write to the shared temporary directory. 2. The attacker predicts that the Skill will use `/tmp/xza_merge_list.txt`. 3. Before the ...[truncated 1421 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions Create a unique, private temporary directory for each merge operation and create the list file exclusively: ```js const os = require("os"); const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "xza-merge-")); const listFile = path.join(tempDir, "inputs.txt"); try { fs.writeFileSync(listFile, content, { encoding: "utf8", flag: "wx", mode: 0o600 }); await runFFmpeg(args); } finally { fs.rmSync(tempDir, { recursive: true, force: true }); } ``` Additional hardening should include: 1. Change `runFFmpeg()` to return a Promise and wait for the child process to exit before deleting the list file. 2. Reject symbolic links if an existing path is ever accepted. 3. Restrict temporary-file permissions to the current user. 4. Avoid a globally shared filename. 5. Validate and correctly escape filenames written to FFmpeg concat-list syntax, including embedded single quotes and line breaks. 6. Avoid running the Skill with elevated privileges. ]]>
