#!/usr/bin/env node /** * patch-lstat-guard.js * P1 安全修复: project-context-injector.js 添加 lstat 符号链接防护 * 红队发现: symlink oracle via .bookworm-context.md (85% 概率) * Idempotent: sentinel 防重复执行 */ 'use strict'; const fs = require('fs'); const path = require('path'); const CLAUDE_ROOT = path.join(process.env.USERPROFILE || process.env.HOME, '.claude'); const HOOKS_DIR = path.join(CLAUDE_ROOT, 'hooks'); const STATE_DIR = path.join(CLAUDE_ROOT, 'session-state'); const SENTINEL = path.join(STATE_DIR, 'patch-lstat-guard.done'); const TARGET = path.join(HOOKS_DIR, 'project-context-injector.js'); if (fs.existsSync(SENTINEL)) { console.log('[SKIP] patch-lstat-guard already applied'); process.exit(0); } if (!fs.existsSync(TARGET)) { console.error('[FAIL] Target not found: ' + TARGET); process.exit(1); } const ANCHOR = ` if (!fs.existsSync(ctxPath)) {\n process.exit(0);\n }`; const PATCH = ` if (!fs.existsSync(ctxPath)) { process.exit(0); } try { if (fs.lstatSync(ctxPath).isSymbolicLink()) process.exit(0); } catch { process.exit(0); }`; try { const bak = TARGET + '.bak-lstat-' + Date.now(); fs.copyFileSync(TARGET, bak); console.log('[OK] Backup: ' + path.basename(bak)); let src = fs.readFileSync(TARGET, 'utf8'); if (src.includes('isSymbolicLink')) { console.log('[SKIP] lstat guard already present in source'); } else if (!src.includes(ANCHOR)) { const anchorCRLF = ANCHOR.replace(/\n/g, '\r\n'); if (src.includes(anchorCRLF)) { src = src.replace(anchorCRLF, PATCH.replace(/\n/g, '\r\n')); console.log('[OK] Patched (CRLF mode)'); } else { console.error('[FAIL] Anchor not found in ' + path.basename(TARGET)); process.exit(1); } } else { src = src.replace(ANCHOR, PATCH); console.log('[OK] Patched (LF mode)'); } const tmp = TARGET + '.tmp.' + process.pid; fs.writeFileSync(tmp, src, 'utf8'); fs.renameSync(tmp, TARGET); console.log('[OK] ' + path.basename(TARGET) + ' updated'); if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true }); fs.writeFileSync(SENTINEL, JSON.stringify({ applied: new Date().toISOString(), target: path.basename(TARGET), backup: path.basename(bak), fix: 'lstat symlink guard after existsSync check' }, null, 2), 'utf8'); console.log('[DONE] patch-lstat-guard applied'); } catch (err) { console.error('[FAIL] ' + err.message); process.exit(1); }