From 2ae76a7a1ba7f266a757a0fa0b99c6817a47b5ed Mon Sep 17 00:00:00 2001 From: Arthur Bassi <201806768+bassi-arthurr@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:36:16 -0300 Subject: [PATCH] ci(security): add payload-guard to verify chain against supply-chain loader --- package.json | 3 +- scripts/payload-guard.mjs | 75 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 scripts/payload-guard.mjs diff --git a/package.json b/package.json index 3df2f4da..8763ae84 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "format": "prettier --write .", "format:check": "prettier --check .", "governance": "node scripts/governance-check.mjs", - "verify": "npm run format:check && npm run lint && npm run build && npm test && npm run governance", + "payload-guard": "node scripts/payload-guard.mjs", + "verify": "npm run payload-guard && npm run format:check && npm run lint && npm run build && npm test && npm run governance", "prepare": "husky" }, "lint-staged": { diff --git a/scripts/payload-guard.mjs b/scripts/payload-guard.mjs new file mode 100644 index 00000000..28af31ef --- /dev/null +++ b/scripts/payload-guard.mjs @@ -0,0 +1,75 @@ +// Supply-chain guard. +// +// Fails the build if the known malicious loader signature is reintroduced into +// tracked source (e.g. a poisoned eslint.config.js). Added after the 2026 +// force-push supply-chain incident. Runs as the first step of `npm run verify`, +// so it executes in the repo-owned governance CI job on every PR and push. +// +// Signatures are the stable markers of the obfuscated, credential-stealing +// loader observed in the attack. Keep the list append-only. Patterns are built +// from fragments so this file never matches itself. + +import { execSync } from "node:child_process"; +import { readFileSync } from "node:fs"; + +const SELF = "scripts/payload-guard.mjs"; + +const CODE_EXT = new Set([".js", ".cjs", ".mjs", ".ts", ".tsx", ".jsx"]); + +const seed = "global" + ".o"; +const shim = "createRequire(import" + ".meta.url)"; + +/** @type {{name:string, test:(path:string, text:string)=>boolean}[]} */ +const RULES = [ + { + name: "loader seed (global.o='8-…')", + test: (_p, t) => new RegExp(seed + "\\s*=\\s*['\"]8-").test(t), + }, + { + name: "permutation variable (_$_…)", + test: (_p, t) => /_\$_/.test(t), + }, + { + name: "delimiter trick (String.fromCharCode(127))", + test: (_p, t) => /String\.fromCharCode\(\s*127\s*\)/.test(t), + }, + { + name: "ESM→CJS shim injected into eslint flat config", + test: (p, t) => /(^|\/)eslint\.config\.(js|mjs|cjs)$/.test(p) && t.includes(shim), + }, +]; + +function trackedFiles() { + const out = execSync("git ls-files", { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }); + return out.split("\n").map((s) => s.trim()).filter(Boolean); +} + +function extOf(path) { + const i = path.lastIndexOf("."); + return i === -1 ? "" : path.slice(i); +} + +const findings = []; +for (const file of trackedFiles()) { + if (file === SELF) continue; + if (!CODE_EXT.has(extOf(file))) continue; + let text; + try { + text = readFileSync(file, "utf8"); + } catch { + continue; + } + for (const rule of RULES) { + if (rule.test(file, text)) findings.push({ file, rule: rule.name }); + } +} + +if (findings.length > 0) { + console.error("payload-guard: known supply-chain markers detected:\n"); + for (const f of findings) console.error(` ✗ ${f.file} — ${f.rule}`); + console.error("\nThis matches the obfuscated loader from the 2026 supply-chain incident."); + console.error("Do not merge. Inspect the file(s) and remove the injected payload."); + process.exit(1); +} + +console.log("payload-guard: no known payload markers found.");