T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/run-cantian.cjs:63
- Finding
- Path Traversal Enables Execution of JavaScript Outside the Cantian Script Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/run-cantian.cjs`, lines 63–94 **Vulnerability Type**: Unrestricted script path traversal leading to local script execution **Risk Level**: Medium ### Vulnerable Code ```js const scriptName = argv[0]; const scriptPath = path.join(CANTIAN_DIR, scriptName); if (!fs.existsSync(scriptPath)) { die(`找不到脚本: ${scriptPath}\n可用脚本:\n ` + fs.readdirSync(CANTIAN_DIR).filter((f) => f.endsWith('.ts')).join('\n '), 2); } // 拆出 --out,其余按顺序透传;@ 前缀展开为文件内容 let outFile = null; const passthrough = []; for (let i = 1; i < argv.length; i += 1) { const token = argv[i]; if (token === '--out') { outFile = argv[i + 1]; if (!outFile) die('--out 后面必须跟一个文件路径', 1); i += 1; continue; } if (token.startsWith('@')) { const source = token.slice(1); if (!fs.existsSync(source)) die(`@ 引用的文件不存在: ${source}`, 1); passthrough.push(fs.readFileSync(source, 'utf8').trim()); continue; } passthrough.push(token); } const result = spawnSync(process.execPath, [scriptPath, ...passthrough], { cwd: CANTIAN_DIR, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, }); ``` ### Technical Analysis The first command-line argument is treated as a script name and joined directly to `CANTIAN_DIR`. The code does not reject absolute paths, directory separators, or `..` traversal components. It also does not canonicalize the resulting path and confirm that it remains inside the intended Cantian directory. `path.join()` normalizes traversal components. Consequently, a value such as `../../some-directory/script.js` can resolve outside `scripts/cantian`. The only validation is `fs.existsSync()`, which verifies existence but not directory containment, file type, extension, ownership, or trust. The resolved path is subsequently passed to `process.execPath`, causing Node.js to execute the selected file. Argument-array execution prevents shell metacharacter injection, but it does not prevent selection and execu ...[truncated 1446 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Replace free-form script selection with an explicit allowlist: ```js const ALLOWED_SCRIPTS = new Set([ 'buildBaziFromLunar.ts', 'buildBaziFromSolar.ts', 'convertToTrueSolarTime.ts', 'getChineseCalendar.ts', 'queryFortuneRange.ts', ]); if (!ALLOWED_SCRIPTS.has(scriptName)) { die('Unsupported Cantian script', 1); } ``` 2. Resolve and verify directory containment as defense in depth: ```js const baseDir = fs.realpathSync(CANTIAN_DIR); const scriptPath = fs.realpathSync(path.resolve(baseDir, scriptName)); if ( path.dirname(scriptPath) !== baseDir || path.extname(scriptPath) !== '.ts' ) { die('Invalid Cantian script path', 1); } ``` 3. Reject absolute paths, null bytes, `..` components, and both POSIX and Windows path separators before resolution. 4. Verify that the target is a regular file rather than a directory or symbolic link. If symbolic links are unnecessary, reject them explicitly. 5. Add regression tests covering `../`, absolute paths, mixed separators, symlink escapes, and valid allowlisted basenames. 6. Continue using `spawnSync()` with an argument array and `shell: false`; this correctly avoids shell injection but must be combined with trusted executable-target selection. ]]>
