#!/usr/bin/env node // Static checks from the Electron security checklist. Pattern-based: it reads files, it runs nothing. // Usage: node check_electron_security.mjs [more dirs] // Exit 1 if anything is found. Checks .js/.mjs/.cjs/.ts/.tsx and .html files, skipping node_modules and build output. import { readdirSync, readFileSync, statSync } from "node:fs"; import { join, relative } from "node:path"; const SKIP = new Set(["node_modules", "dist", "out", "build", ".git", ".vite", "release"]); const CODE = /\.(?:[cm]?js|tsx?)$/; function walk(dir, files = []) { for (const name of readdirSync(dir)) { if (SKIP.has(name)) continue; const p = join(dir, name); if (statSync(p).isDirectory()) walk(p, files); else if (CODE.test(name) || name.endsWith(".html")) files.push(p); } return files; } const LINE_RULES = [ [/\bnodeIntegration(?:InWorker|InSubFrames)?\s*:\s*true/, "nodeIntegration enabled: renderer code gets Node.js (checklist 2)"], [/\bcontextIsolation\s*:\s*false/, "contextIsolation disabled (checklist 3)"], [/\bsandbox\s*:\s*false/, "sandbox disabled (checklist 4)"], [/\bwebSecurity\s*:\s*false/, "webSecurity disabled (checklist 6)"], [/\ballowRunningInsecureContent\s*:\s*true/, "allowRunningInsecureContent enabled (checklist 8)"], [/\bexperimentalFeatures\s*:\s*true/, "experimentalFeatures enabled (checklist 9)"], [/\benableBlinkFeatures\s*:/, "enableBlinkFeatures used (checklist 10)"], [/\ballowpopups\b/, "allowpopups on a webview (checklist 11)"], [/@electron\/remote|\benableRemoteModule\s*:\s*true/, "remote module: renderer reaches main objects directly"], [/exposeInMainWorld\([^)]*\bipcRenderer\b\s*\)/, "ipcRenderer exposed to the renderer (checklist 20)"], [/exposeInMainWorld\([\s\S]*?\b(?:send|invoke|on)\s*:\s*\(\s*channel\b/, "renderer chooses the IPC channel name (checklist 20)"], [/\bipcRenderer\.(?:send|invoke)\(\s*channel\b/, "IPC channel name comes from a variable; use fixed names"], [/\bloadURL\(\s*["'`]http:\/\//, "loads content over plain HTTP (checklist 1)"], [/\bloadURL\(\s*["'`]file:\/\//, "file:// URL; serve the bundle from a custom protocol (checklist 18)"], [/\bRunAsNode\]\s*:\s*true/, "RunAsNode fuse left on (checklist 19)"], [/unsafe-eval/, "CSP allows unsafe-eval (checklist 7)"], ]; const findings = []; const roots = process.argv.slice(2); if (!roots.length) { console.error("usage: node check_electron_security.mjs [more dirs]"); process.exit(2); } let mainFiles = 0; let htmlFiles = 0; for (const root of roots) { for (const file of walk(root)) { const rel = relative(process.cwd(), file); const text = readFileSync(file, "utf8"); const lines = text.split("\n"); lines.forEach((line, i) => { for (const [re, msg] of LINE_RULES) if (re.test(line)) findings.push(`${rel}:${i + 1}: ${msg}`); }); if (file.endsWith(".html")) { htmlFiles++; if (!/Content-Security-Policy/i.test(text)) findings.push(`${rel}: no Content-Security-Policy meta tag (checklist 7)`); continue; } if (/new BrowserWindow\(/.test(text)) { mainFiles++; if (!/setWindowOpenHandler\(/.test(text)) findings.push(`${rel}: BrowserWindow without setWindowOpenHandler (checklist 14)`); if (!/["']will-navigate["']/.test(text)) findings.push(`${rel}: no will-navigate handler limiting navigation (checklist 13)`); } const handlers = [...text.matchAll(/ipcMain\.(?:handle|on)\(\s*["'`]([^"'`]+)["'`]\s*,\s*(?:async\s*)?\(([^)]*)\)\s*=>\s*\{([\s\S]*?)\n\}\);/g)]; for (const [, channel, , body] of handlers) { if (!/senderFrame|assertTrustedSender|validateSender/.test(body)) findings.push(`${rel}: IPC handler "${channel}" does not check event.senderFrame (checklist 17)`); } for (const m of text.matchAll(/shell\.openExternal\(([^)]*)\)/g)) { const before = text.slice(Math.max(0, m.index - 300), m.index); if (!/^["'`]https:\/\//.test(m[1].trim()) && !/protocol|isAllowed|allowList|allowlist|startsWith\(\s*["'`]https:/.test(before)) { findings.push(`${rel}: shell.openExternal(${m[1].trim()}) without a URL check (checklist 15)`); } } } } for (const f of findings) console.log(f); console.log(findings.length ? `${findings.length} finding(s)` : `ok: no findings (${mainFiles} window file(s), ${htmlFiles} HTML file(s))`); process.exit(findings.length ? 1 : 0);