T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/md-slim.js:60
- Finding
- Workspace Path Traversal Enables Out-of-Scope File Modification<![CDATA[ ## Vulnerability Details **File Location**: `scripts/md-slim.js`, lines 60-92 and 122-130 **Vulnerability Type**: Path traversal and insufficient filesystem boundary validation **Risk Level**: Medium ### Vulnerable Code ```js function applySlim(file, titles, dry) { const p = path.join(L.WORKSPACE, file); const text = fs.readFileSync(p, 'utf8'); const { preamble, sections } = splitSections(text); const picked = sections.filter((s) => titles.includes(s.title)); if (!picked.length) { console.error(`❌ 未找到指定小节:${titles.join(' / ')}`); process.exit(1); } const date = new Date().toISOString().slice(0, 10); const dir = path.dirname(p); const archiveDir = path.join(dir, 'archive'); const base = path.basename(file, '.md'); const results = []; if (!dry) { const bakDir = path.join(L.WORKSPACE, 'backups', 'md-slim'); fs.mkdirSync(bakDir, { recursive: true }); fs.copyFileSync(p, path.join(bakDir, `${base}.${new Date().toISOString().replace(/[:.]/g, '-')}.bak`)); fs.mkdirSync(archiveDir, { recursive: true }); } const before = Buffer.byteLength(text, 'utf8'); let newText = text; for (const s of picked) { const slug = s.title.replace(/[^\w\u4e00-\u9fa5-]+/g, '-').slice(0, 30); const arcRel = path.relative(L.WORKSPACE, path.join(archiveDir, `${base}-${slug}-${date}.md`)); const arcContent = `# ${base} · ${s.title}(归档 ${date})\n\n> 由 md-slim.js 从 \`${file}\` 移出。\n> 原文完整保留于此;主文件保留指针,仍可用 memory_search 检索到。\n\n${s.body}\n`; if (!dry) fs.writeFileSync(path.join(L.WORKSPACE, arcRel), arcContent); const pointer = `## ${s.title}\n\n> 📦 已归档:\`${arcRel}\`(${(s.bytes / 1024).toFixed(1)}KB,内容完整保留,可用检索取回)\n`; newText = newText.replace(s.body, pointer.trimEnd()); results.push({ title: s.title, kb: (s.bytes / 1024).toFixed(1), archive: arcRel, movedBytes: s.bytes, arcBytes: Buffer.byteLength(arcContent, 'utf8') }); } if (!dry) fs.writeFileSync(p, newText); } ``` The command-line value is passed int ...[truncated 3052 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions Resolve and validate every source and destination against a canonical workspace root before performing any filesystem operation: ```js function resolveInsideWorkspace(relativePath) { const root = path.resolve(L.WORKSPACE); const target = path.resolve(root, relativePath); if (target !== root && !target.startsWith(root + path.sep)) { throw new Error('The requested path must remain inside the workspace'); } return target; } ``` Use it for the source file and all generated archive and backup paths: ```js const p = resolveInsideWorkspace(file); ``` Additional hardening should include: 1. Restrict `--file` to `L.WORKSPACE_CONTEXT_FILES` unless arbitrary workspace files are explicitly required. 2. Reject absolute paths and any input containing traversal components. 3. Use `fs.realpathSync()` for existing files and verify containment after resolving symbolic links. 4. Reject symbolic-link source files or destinations when symlinks are not required. 5. Validate the parent of newly created archive files through `realpathSync()` before writing. 6. Add tests for `../`, absolute paths, nested traversal, and symlinks escaping the workspace. ]]>
