From d396a82bd91c64f7e2867ee99ee310c70feb5b7e Mon Sep 17 00:00:00 2001 From: JasonOA888 Date: Tue, 5 May 2026 19:57:52 +0800 Subject: [PATCH] fix: path traversal via crafted .md filenames + command injection in --open Two vulnerabilities across company-research and event-prospecting compile_report.mjs: 1. Path traversal: slug is derived directly from the markdown filename via file.replace('.md', ''). A crafted filename like "../../etc/evil.md" produces slug "../../etc/evil" which, when passed to join(dir, 'companies', `${slug}.html`), writes outside the intended output directory. Fix: strip ../ and path separators from the slug before use. 2. Command injection (--open): execSync(`open "${path}"`) passes the user-provided directory name through a shell. Directory names containing $(cmd) or `cmd` are executed by bash inside the double-quoted string. Fix: use execFileSync('open', [path]) which bypasses shell interpolation entirely. --- skills/company-research/scripts/compile_report.mjs | 8 +++++--- skills/event-prospecting/scripts/compile_report.mjs | 7 ++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/skills/company-research/scripts/compile_report.mjs b/skills/company-research/scripts/compile_report.mjs index 2759c69..bb90a40 100644 --- a/skills/company-research/scripts/compile_report.mjs +++ b/skills/company-research/scripts/compile_report.mjs @@ -169,7 +169,9 @@ for (const file of files) { const fields = parseFrontmatter(content); if (!fields) continue; const body = parseBody(content); - const slug = file.replace('.md', ''); + // Sanitize slug to prevent path traversal via crafted filenames + // (e.g. "../../etc/evil.md" → "evilevil" without "../" components) + const slug = file.replace('.md', '').replace(/\.\./g, '').replace(/[\\/]/g, ''); companies.push({ ...fields, body, slug, file }); } @@ -351,6 +353,6 @@ console.log(join(dir, 'index.html')); // Open in browser if requested if (shouldOpen) { - const { execSync } = await import('child_process'); - try { execSync(`open "${join(dir, 'index.html')}"`); } catch {} + const { execFileSync } = await import('child_process'); + try { execFileSync('open', [join(dir, 'index.html')]); } catch {} } diff --git a/skills/event-prospecting/scripts/compile_report.mjs b/skills/event-prospecting/scripts/compile_report.mjs index e0f55c6..8106012 100644 --- a/skills/event-prospecting/scripts/compile_report.mjs +++ b/skills/event-prospecting/scripts/compile_report.mjs @@ -273,7 +273,8 @@ function readMdDir(p) { const fields = parseFrontmatter(content); if (!fields) return null; const body = parseBody(content); - const slug = f.replace('.md', ''); + // Sanitize slug to prevent path traversal via crafted filenames + const slug = f.replace('.md', '').replace(/\.\./g, '').replace(/[\\/]/g, ''); return { ...fields, body, slug, file: f }; }).filter(Boolean); } @@ -907,6 +908,6 @@ console.error(JSON.stringify({ console.log(join(dir, 'index.html')); if (shouldOpen) { - const { execSync } = await import('child_process'); - try { execSync(`open "${join(dir, 'index.html')}"`); } catch {} + const { execFileSync } = await import('child_process'); + try { execFileSync('open', [join(dir, 'index.html')]); } catch {} }