From 4cc98e7c0fe7e2aa5dba353b2e26d6bf17e6e079 Mon Sep 17 00:00:00 2001 From: Selig Date: Sat, 14 Mar 2026 15:03:34 +0800 Subject: [PATCH] improve(qmd-brain): harden command execution against injection --- skills/qmd-brain/handler.ts | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/skills/qmd-brain/handler.ts b/skills/qmd-brain/handler.ts index c226b3d..f0dda98 100644 --- a/skills/qmd-brain/handler.ts +++ b/skills/qmd-brain/handler.ts @@ -7,13 +7,15 @@ * - embed_to_pg.py (Python venv at /home/selig/apps/qmd-pg/) */ -import { execSync, exec } from 'child_process'; +import { exec, execFile } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); +const execFileAsync = promisify(execFile); const QMD_CMD = '/home/selig/.nvm/versions/node/v24.13.1/bin/qmd'; -const EMBED_PY = '/home/selig/apps/qmd-pg/venv/bin/python3 /home/selig/apps/qmd-pg/embed_to_pg.py'; +const EMBED_PY_BIN = '/home/selig/apps/qmd-pg/venv/bin/python3'; +const EMBED_PY_SCRIPT = '/home/selig/apps/qmd-pg/embed_to_pg.py'; const MAX_SEARCH_LEN = 1500; // 回覆中搜尋結果最大字數 interface SearchResult { @@ -23,17 +25,12 @@ interface SearchResult { similarity?: number; } -interface QmdResult { - path: string; - text?: string; - score?: number; -} - /** 執行 qmd BM25 全文搜尋 */ async function qmdSearch(query: string, topK = 5): Promise { try { - const { stdout } = await execAsync( - `${QMD_CMD} search ${JSON.stringify(query)} --output markdown --limit ${topK}`, + const { stdout } = await execFileAsync( + QMD_CMD, + ['search', query, '--output', 'markdown', '--limit', String(topK)], { timeout: 15000, env: { ...process.env, HOME: '/home/selig' } } ); return stdout.trim() || '(無結果)'; @@ -45,8 +42,9 @@ async function qmdSearch(query: string, topK = 5): Promise { /** 執行 PostgreSQL 向量語意搜尋 */ async function pgSearch(query: string, topK = 5): Promise { try { - const { stdout } = await execAsync( - `${EMBED_PY} search ${JSON.stringify(query)} --top-k ${topK} --json`, + const { stdout } = await execFileAsync( + EMBED_PY_BIN, + [EMBED_PY_SCRIPT, 'search', query, '--top-k', String(topK), '--json'], { timeout: 20000 } ); return JSON.parse(stdout) as SearchResult[]; @@ -71,7 +69,7 @@ async function triggerEmbed(): Promise { try { // 背景執行,不等待完成 exec( - `${QMD_CMD} embed 2>&1 >> /tmp/qmd-embed.log & ${EMBED_PY} embed 2>&1 >> /tmp/qmd-embed.log &`, + `${QMD_CMD} embed 2>&1 >> /tmp/qmd-embed.log & ${EMBED_PY_BIN} ${EMBED_PY_SCRIPT} embed 2>&1 >> /tmp/qmd-embed.log &`, { env: { ...process.env, HOME: '/home/selig' } } ); return '✅ 索引更新已在背景啟動,約需 1-5 分鐘完成。'; @@ -86,8 +84,9 @@ async function getStats(): Promise { // qmd collection list try { - const { stdout } = await execAsync( - `${QMD_CMD} collection list`, + const { stdout } = await execFileAsync( + QMD_CMD, + ['collection', 'list'], { timeout: 5000, env: { ...process.env, HOME: '/home/selig' } } ); results.push(`**qmd Collections:**\n\`\`\`\n${stdout.trim()}\n\`\`\``); @@ -97,8 +96,9 @@ async function getStats(): Promise { // pgvector stats try { - const { stdout } = await execAsync( - `${EMBED_PY} stats`, + const { stdout } = await execFileAsync( + EMBED_PY_BIN, + [EMBED_PY_SCRIPT, 'stats'], { timeout: 10000 } ); results.push(`**PostgreSQL pgvector:**\n\`\`\`\n${stdout.trim()}\n\`\`\``);