mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-07-30 07:27:44 -04:00
122 lines
3.8 KiB
JavaScript
122 lines
3.8 KiB
JavaScript
import fs from 'fs';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
const SCAN_DIRS = ['src/js', 'bentopdf-pymupdf-wasm/src', 'cloudflare'];
|
|
const SKIP = ['node_modules', 'dist', '.git', 'coverage', 'tests', 'test'];
|
|
|
|
const findings = [];
|
|
|
|
function walk(dir) {
|
|
let entries;
|
|
try {
|
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
} catch {
|
|
return;
|
|
}
|
|
for (const e of entries) {
|
|
const full = path.join(dir, e.name);
|
|
if (
|
|
SKIP.some(
|
|
(s) =>
|
|
full.includes(`${path.sep}${s}${path.sep}`) ||
|
|
full.endsWith(`${path.sep}${s}`)
|
|
)
|
|
) {
|
|
continue;
|
|
}
|
|
if (e.isDirectory()) walk(full);
|
|
else if (/\.(ts|js|mjs)$/.test(e.name) && !/\.test\.|\.d\.ts$/.test(e.name))
|
|
checkFile(full);
|
|
}
|
|
}
|
|
|
|
function checkFile(file) {
|
|
const rel = path.relative(ROOT, file);
|
|
const src = fs.readFileSync(file, 'utf-8');
|
|
const lines = src.split('\n');
|
|
|
|
// Rule 1: no raw string interpolation into a Python/eval template.
|
|
const codeSink = /(runPython|\.eval)\s*\(\s*`/;
|
|
for (let i = 0; i < lines.length; i++) {
|
|
if (codeSink.test(lines[i])) {
|
|
for (let j = i; j < Math.min(i + 60, lines.length); j++) {
|
|
const interps = lines[j].match(/\$\{[^}]+\}/g) || [];
|
|
for (const it of interps) {
|
|
const danger =
|
|
/^\$\{\s*(text|name|uri|url|format|password|passwd|title|label|filename|content|search|searchText|query|font|fontname|intent|usage|replaceText)\s*(\}|\.|\))/.test(
|
|
it
|
|
);
|
|
const safe = /pyStr|JSON\.stringify|escapeHtml/.test(it);
|
|
if (danger && !safe) {
|
|
findings.push({
|
|
rule: 'code-injection',
|
|
file: rel,
|
|
line: j + 1,
|
|
detail: `Unescaped user-string interpolation ${it} into a Python/eval template — pass it via pyStr()/JSON.stringify()/globals.set().`,
|
|
});
|
|
}
|
|
}
|
|
if (lines[j].includes('`)') || lines[j].trim().endsWith('`);')) break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Rule 2: string mutation applied AFTER a sanitize call (sanitize-then-mutate).
|
|
const sanitizeLine = lines.findIndex((l) =>
|
|
/DOMPurify\.sanitize|sanitizeEmailHtml|sanitize\(/.test(l)
|
|
);
|
|
if (sanitizeLine !== -1) {
|
|
for (
|
|
let j = sanitizeLine + 1;
|
|
j < Math.min(sanitizeLine + 40, lines.length);
|
|
j++
|
|
) {
|
|
if (
|
|
/\.replace\(|innerHTML\s*=|\+=\s*`/.test(lines[j]) &&
|
|
!/escapeHtml|escapeHTML|DOMPurify/.test(lines[j])
|
|
) {
|
|
findings.push({
|
|
rule: 'sanitize-then-mutate',
|
|
file: rel,
|
|
line: j + 1,
|
|
detail: `Possible mutation of sanitized HTML. Escape/validate at the final sink, or re-sanitize after mutation.`,
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const d of SCAN_DIRS) walk(path.join(ROOT, d));
|
|
|
|
const blocking = findings.filter((f) => f.rule === 'code-injection');
|
|
const warnings = findings.filter((f) => f.rule !== 'code-injection');
|
|
|
|
for (const f of warnings) {
|
|
console.warn(` ⚠ [${f.rule}] ${f.file}:${f.line}\n ${f.detail}`);
|
|
}
|
|
if (warnings.length) {
|
|
console.warn(
|
|
`\n${warnings.length} sanitize-then-mutate warning(s) — review each: escape/validate at the final sink.\n`
|
|
);
|
|
}
|
|
|
|
if (blocking.length === 0) {
|
|
console.log('✅ security-patterns-check: no code-injection patterns found');
|
|
process.exit(0);
|
|
}
|
|
|
|
console.error(
|
|
`\n❌ security-patterns-check found ${blocking.length} code-injection issue(s):\n`
|
|
);
|
|
for (const f of blocking) {
|
|
console.error(` [${f.rule}] ${f.file}:${f.line}\n ${f.detail}\n`);
|
|
}
|
|
console.error(
|
|
'This pattern caused a prior CVE (pymupdf runPython injection). Pass values via pyStr()/JSON.stringify()/globals.set().\n'
|
|
);
|
|
process.exit(1);
|