blitzstrike 2.4.59 → 2.4.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7385,6 +7385,69 @@ var init_version = __esm(() => {
7385
7385
  })();
7386
7386
  });
7387
7387
 
7388
+ // src/secrets.ts
7389
+ function shannonEntropy(s) {
7390
+ if (!s)
7391
+ return 0;
7392
+ const freq = new Map;
7393
+ for (const c of s)
7394
+ freq.set(c, (freq.get(c) ?? 0) + 1);
7395
+ let e = 0;
7396
+ for (const n of freq.values()) {
7397
+ const p = n / s.length;
7398
+ e -= p * Math.log2(p);
7399
+ }
7400
+ return e;
7401
+ }
7402
+ function redactSecrets(text) {
7403
+ let out = text;
7404
+ for (const [re, repl] of REDACTION_PATTERNS) {
7405
+ out = out.replace(re, repl);
7406
+ }
7407
+ return out;
7408
+ }
7409
+ var SECRET_FIELDS = "api[_-]?key|apikey|api[_-]?secret|access[_-]?key|access[_-]?token|secret[_-]?key|secret[_-]?access[_-]?key|auth[_-]?token|client[_-]?secret|consumer[_-]?secret|consumer[_-]?key|private[_-]?key|db[_-]?password|database[_-]?password|db[_-]?pass|passwd|password|jwt[_-]?secret|encryption[_-]?key|encryption[_-]?password|github[_-]?token|gitlab[_-]?token|stripe[_-]?key|sendgrid[_-]?api[_-]?key|datadog[_-]?api[_-]?key|cloudflare[_-]?api[_-]?key|slack[_-]?token|aws[_-]?access[_-]?key[_-]?id|aws[_-]?secret[_-]?access[_-]?key|aws[_-]?session[_-]?token|bot[_-]?token", SECRET_FORMATS, REDACTION_PATTERNS;
7410
+ var init_secrets = __esm(() => {
7411
+ SECRET_FORMATS = [
7412
+ { name: "JWT", re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}/g },
7413
+ { name: "AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/g },
7414
+ { name: "GitHub PAT (classic)", re: /\bghp_[A-Za-z0-9]{20,}\b/g },
7415
+ { name: "GitHub PAT (fine-grained)", re: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g },
7416
+ { name: "OpenAI/Stripe key", re: /\bsk-[A-Za-z0-9]{20,}\b/g },
7417
+ { name: "Stripe live/test key", re: /\b(?:sk|pk|rk)_(?:live|test)_[0-9a-zA-Z]{10,}\b/g },
7418
+ { name: "Slack token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
7419
+ { name: "Google API key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
7420
+ { name: "Twilio account SID", re: /\bAC[0-9a-f]{32}\b/g },
7421
+ { name: "SendGrid API key", re: /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g },
7422
+ { name: "Mailgun API key", re: /\bkey-[0-9a-zA-Z]{32}\b/g },
7423
+ { name: "PEM private key", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/g }
7424
+ ];
7425
+ REDACTION_PATTERNS = [
7426
+ [/(authorization\s*[:=]\s*)(bearer\s+)?[A-Za-z0-9._~+/=-]{8,}/gi, "$1$2[REDACTED]"],
7427
+ [/(api[_-]?key\s*[:=]\s*)["']?[A-Za-z0-9._-]{16,}["']?/gi, "$1[REDACTED]"],
7428
+ [/(access[_-]?token\s*[:=]\s*)["']?[A-Za-z0-9._-]{16,}["']?/gi, "$1[REDACTED]"],
7429
+ [/(secret\s*[:=]\s*)["']?[A-Za-z0-9._/-]{16,}["']?/gi, "$1[REDACTED]"],
7430
+ [/(password\s*[:=]\s*)["']?[^"'\s,}&]{4,}["']?/gi, "$1[REDACTED]"],
7431
+ [/(passwd\s*[:=]\s*)["']?[^"'\s,}&]{4,}["']?/gi, "$1[REDACTED]"],
7432
+ [/(private[_-]?key\s*[:=]\s*)["']?[A-Za-z0-9+/=_-]{16,}["']?/gi, "$1[REDACTED]"],
7433
+ [/(session\s*[:=]\s*)["']?[A-Za-z0-9._-]{12,}["']?/gi, "$1[REDACTED]"],
7434
+ [/(cookie\s*[:=]\s*)["']?[A-Za-z0-9._-]{12,}["']?/gi, "$1[REDACTED]"],
7435
+ [/(set-cookie\s*:\s*)[^\r\n]+/gi, "$1[REDACTED]"],
7436
+ [/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}/g, "[REDACTED]"],
7437
+ [/AKIA[0-9A-Z]{16}/g, "[REDACTED]"],
7438
+ [/ghp_[A-Za-z0-9]{20,}/g, "[REDACTED]"],
7439
+ [/github_pat_[A-Za-z0-9_]{20,}/g, "[REDACTED]"],
7440
+ [/sk-[A-Za-z0-9]{20,}/g, "[REDACTED]"],
7441
+ [/\b(?:sk|pk|rk)_(?:live|test)_[0-9a-zA-Z]{10,}\b/g, "[REDACTED]"],
7442
+ [/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, "[REDACTED]"],
7443
+ [/\bAIza[0-9A-Za-z_-]{35}\b/g, "[REDACTED]"],
7444
+ [/\bAC[0-9a-f]{32}\b/g, "[REDACTED]"],
7445
+ [/\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g, "[REDACTED]"],
7446
+ [/\bkey-[0-9a-zA-Z]{32}\b/g, "[REDACTED]"],
7447
+ [/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[REDACTED]"]
7448
+ ];
7449
+ });
7450
+
7388
7451
  // src/http.ts
7389
7452
  function researchHeaders() {
7390
7453
  const u = process.env.H1_USERNAME?.trim();
@@ -15489,256 +15552,1451 @@ async function checkMcpServers() {
15489
15552
  }
15490
15553
  var init_mcp_status = () => {};
15491
15554
 
15492
- // src/sync.ts
15493
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, readFileSync as readFileSync24, readdirSync as readdirSync7, statSync as statSync4, rmSync as rmSync4 } from "node:fs";
15494
- import { join as join21 } from "node:path";
15495
- import { homedir as homedir10 } from "node:os";
15496
- import { fileURLToPath as fileURLToPath11 } from "node:url";
15497
- import { spawnSync as spawnSync4 } from "node:child_process";
15498
- function dirExists(p) {
15499
- try {
15500
- return statSync4(p).isDirectory();
15501
- } catch {
15502
- return false;
15503
- }
15504
- }
15505
- function countFiles(dir) {
15506
- try {
15507
- let n = 0;
15508
- const walk = (d) => {
15509
- for (const e of readdirSync7(d, { withFileTypes: true })) {
15510
- if (e.isDirectory())
15511
- walk(join21(d, e.name));
15512
- else
15513
- n++;
15555
+ // src/security-state.ts
15556
+ function analyzeSecurityState(input) {
15557
+ let state = "tainted";
15558
+ let context = null;
15559
+ for (const fn of input.sanitizers) {
15560
+ const s = SANITIZERS2[fn];
15561
+ if (!s)
15562
+ continue;
15563
+ if (s.level === "validated") {
15564
+ if (state === "tainted" || state === "sanitized") {
15565
+ state = "validated";
15566
+ context = s.context;
15514
15567
  }
15515
- };
15516
- walk(dir);
15517
- return n;
15518
- } catch {
15519
- return 0;
15568
+ } else if (state === "tainted") {
15569
+ state = "sanitized";
15570
+ context = s.context;
15571
+ }
15520
15572
  }
15521
- }
15522
- function syncData() {
15523
- if (!dirExists(DATA_ROOT2))
15524
- mkdirSync9(DATA_ROOT2, { recursive: true });
15525
- const payloadsPath = join21(DATA_ROOT2, "payloads");
15526
- const templatesPath = join21(DATA_ROOT2, "templates");
15527
- const hasPayloads = dirExists(payloadsPath) && countFiles(payloadsPath) > 0;
15528
- const hasTemplates = dirExists(templatesPath) && countFiles(templatesPath) > 0;
15529
- if (hasPayloads && hasTemplates) {
15530
- return {
15531
- payloads: { path: payloadsPath, files: countFiles(payloadsPath), synced: false },
15532
- templates: { path: templatesPath, files: countFiles(templatesPath), synced: false },
15533
- data_root: DATA_ROOT2
15534
- };
15573
+ const required = SINK_REQUIREMENTS[input.sinkType] ?? [];
15574
+ if (required.length === 0) {
15575
+ return { verdict: "unknown", state, context, required, reason: `sink type '${input.sinkType}' not in the requirements table` };
15535
15576
  }
15536
- const localPayloads = join21(ROOT7, "payloads");
15537
- const localTemplates = join21(ROOT7, "templates");
15538
- let payloadsSynced = false;
15539
- let templatesSynced = false;
15540
- if (!hasPayloads && dirExists(localPayloads)) {
15541
- copyDir(localPayloads, payloadsPath);
15542
- payloadsSynced = true;
15577
+ if (state === "tainted") {
15578
+ return { verdict: "vulnerable", state, context, required, reason: `raw tainted input reaches a ${input.sinkType} sink with no sanitization` };
15543
15579
  }
15544
- if (!hasTemplates && dirExists(localTemplates)) {
15545
- copyDir(localTemplates, templatesPath);
15546
- templatesSynced = true;
15580
+ if (state === "validated") {
15581
+ return { verdict: "safe", state, context, required, reason: `input validated (${context}) — safe for any sink` };
15547
15582
  }
15548
- const gitOk = spawnSync4("git", ["--version"]).status === 0;
15549
- if (gitOk && (!dirExists(payloadsPath) || countFiles(payloadsPath) === 0 || !dirExists(templatesPath) || countFiles(templatesPath) === 0)) {
15550
- const tmp = join21(DATA_ROOT2, ".tmp-clone");
15551
- if (dirExists(tmp))
15552
- rmSync4(tmp, { recursive: true, force: true });
15553
- const r = spawnSync4("git", ["clone", "--depth", "1", "--filter=blob:none", "--sparse", GITHUB_REPO, tmp], { timeout: 120000 });
15554
- if (r.status === 0) {
15555
- const sparse = spawnSync4("git", ["-C", tmp, "sparse-checkout", "set", "payloads", "templates"], { timeout: 120000 });
15556
- if (sparse.status === 0) {
15557
- if (!dirExists(payloadsPath) || countFiles(payloadsPath) === 0) {
15558
- copyDir(join21(tmp, "payloads"), payloadsPath);
15559
- payloadsSynced = true;
15560
- }
15561
- if (!dirExists(templatesPath) || countFiles(templatesPath) === 0) {
15562
- copyDir(join21(tmp, "templates"), templatesPath);
15563
- templatesSynced = true;
15564
- }
15565
- }
15566
- rmSync4(tmp, { recursive: true, force: true });
15583
+ if (state === "sanitized") {
15584
+ if (context && required.includes(context)) {
15585
+ return { verdict: "safe", state, context, required, reason: `sanitized for '${context}' — matches sink requirement ${required.join("/")}` };
15567
15586
  }
15587
+ return { verdict: "vulnerable", state, context, required, reason: `sanitized for '${context}' but this sink requires ${required.join("/")} — WRONG sanitizer (context mismatch)` };
15568
15588
  }
15569
- return {
15570
- payloads: { path: payloadsPath, files: countFiles(payloadsPath), synced: payloadsSynced },
15571
- templates: { path: templatesPath, files: countFiles(templatesPath), synced: templatesSynced },
15572
- data_root: DATA_ROOT2
15573
- };
15589
+ return { verdict: "unknown", state, context, required, reason: "unhandled state" };
15574
15590
  }
15575
- function copyDir(src, dst) {
15576
- const r = spawnSync4("cp", ["-r", src, dst], { timeout: 120000 });
15577
- if (r.status !== 0) {
15578
- manualCopy(src, dst);
15579
- }
15591
+ function sinkTypeOf(line) {
15592
+ if (/\$wpdb->(query|get_var|get_row|get_results|get_col)|mysqli_query\s*\(|mysql_query\s*\(|pg_query\s*\(|->query\s*\(/.test(line) && !/->prepare\s*\(/.test(line))
15593
+ return "sql_execution";
15594
+ if (/\b(eval|assert)\s*\(/.test(line))
15595
+ return "code_execution";
15596
+ if (/\b(system|exec|shell_exec|passthru|proc_open|popen)\s*\(/.test(line))
15597
+ return "command_execution";
15598
+ if (/\b(unserialize|maybe_unserialize)\s*\(/.test(line))
15599
+ return "deserialization";
15600
+ if (/\b(file_get_contents|file_put_contents|fopen|readfile|include|require|include_once|require_once)\s*\(/.test(line))
15601
+ return "file_operations";
15602
+ if (/header\s*\(\s*['"]Location/i.test(line))
15603
+ return "redirect";
15604
+ return null;
15580
15605
  }
15581
- function manualCopy(src, dst) {
15582
- mkdirSync9(dst, { recursive: true });
15583
- for (const e of readdirSync7(src, { withFileTypes: true })) {
15584
- const s = join21(src, e.name);
15585
- const d = join21(dst, e.name);
15586
- if (e.isDirectory())
15587
- manualCopy(s, d);
15588
- else
15589
- writeFileSync8(d, readFileSync24(s));
15606
+ function detectWrongSanitizer(code, file) {
15607
+ const out = [];
15608
+ const ls = code.split(`
15609
+ `);
15610
+ const varSanitizer = new Map;
15611
+ for (const line of ls) {
15612
+ const m = line.match(/\$(\w+)\s*=\s*([a-zA-Z0-9_]+)\s*\(\s*([^)]*)\s*\)\s*;?/);
15613
+ if (!m)
15614
+ continue;
15615
+ const [, varName, fn, arg] = m;
15616
+ if (!SOURCE_TOKENS2.test(arg))
15617
+ continue;
15618
+ if (SANITIZERS2[fn])
15619
+ varSanitizer.set(varName, fn);
15620
+ else if (PSEUDO_SANITIZERS.has(fn))
15621
+ varSanitizer.set(varName, fn);
15622
+ }
15623
+ for (let i = 0;i < ls.length; i++) {
15624
+ const line = ls[i];
15625
+ const sinkType = sinkTypeOf(line);
15626
+ if (!sinkType)
15627
+ continue;
15628
+ const required = SINK_REQUIREMENTS[sinkType] ?? [];
15629
+ for (const [varName, fn] of varSanitizer) {
15630
+ if (!new RegExp(`\\$${varName}\\b`).test(line))
15631
+ continue;
15632
+ const sanitizer = SANITIZERS2[fn];
15633
+ const context = sanitizer?.context ?? null;
15634
+ if (sanitizer && (sanitizer.level === "validated" || context && required.includes(context)))
15635
+ continue;
15636
+ const detail = sanitizer ? `$${varName} was sanitized for '${context}' (${fn}()) but this ${sinkType} sink requires ${required.join("/")} — WRONG sanitizer (context mismatch).` : `$${varName} passed through ${fn}() — a NON-sanitizing transformation — and still reaches this ${sinkType} sink as raw tainted input. ${fn}() does not neutralize ${required.join("/")} context.`;
15637
+ out.push({
15638
+ file,
15639
+ line: i + 1,
15640
+ type: "wrong_sanitizer",
15641
+ category: "Wrong-context sanitization / pseudo-sanitizer",
15642
+ severity: "high",
15643
+ evidence: line.trim(),
15644
+ detail
15645
+ });
15646
+ }
15590
15647
  }
15648
+ const seen = new Set;
15649
+ return out.filter((f) => {
15650
+ const key = `${f.type}:${f.line}`;
15651
+ if (seen.has(key))
15652
+ return false;
15653
+ seen.add(key);
15654
+ return true;
15655
+ });
15591
15656
  }
15592
- var ROOT7, DATA_ROOT2, GITHUB_REPO = "https://github.com/shinthink/blitzstrike.git";
15593
- var init_sync = __esm(() => {
15594
- ROOT7 = join21(fileURLToPath11(new URL(".", import.meta.url)), "..");
15595
- DATA_ROOT2 = process.env.BLITZSTRIKE_DATA ?? join21(homedir10(), ".blitzstrike", "data");
15657
+ var SANITIZERS2, PSEUDO_SANITIZERS, SINK_REQUIREMENTS, SOURCE_TOKENS2;
15658
+ var init_security_state = __esm(() => {
15659
+ SANITIZERS2 = {
15660
+ esc_sql: { context: "sql", level: "sanitized" },
15661
+ addslashes: { context: "sql", level: "sanitized" },
15662
+ mysqli_real_escape_string: { context: "sql", level: "sanitized" },
15663
+ mysql_real_escape_string: { context: "sql", level: "sanitized" },
15664
+ pg_escape_string: { context: "sql", level: "sanitized" },
15665
+ sanitize_text_field: { context: "html", level: "sanitized" },
15666
+ sanitize_textarea_field: { context: "html", level: "sanitized" },
15667
+ esc_html: { context: "html", level: "sanitized" },
15668
+ esc_html__: { context: "html", level: "sanitized" },
15669
+ htmlspecialchars: { context: "html", level: "sanitized" },
15670
+ htmlentities: { context: "html", level: "sanitized" },
15671
+ wp_kses: { context: "html", level: "sanitized" },
15672
+ wp_kses_post: { context: "html", level: "sanitized" },
15673
+ wp_kses_data: { context: "html", level: "sanitized" },
15674
+ strip_tags: { context: "html", level: "sanitized" },
15675
+ esc_attr: { context: "html_attr", level: "sanitized" },
15676
+ esc_url: { context: "url", level: "sanitized" },
15677
+ esc_url_raw: { context: "url", level: "sanitized" },
15678
+ esc_textarea: { context: "html", level: "sanitized" },
15679
+ esc_js: { context: "js", level: "sanitized" },
15680
+ urlencode: { context: "url", level: "sanitized" },
15681
+ rawurlencode: { context: "url", level: "sanitized" },
15682
+ wp_json_encode: { context: "json", level: "sanitized" },
15683
+ json_encode: { context: "json", level: "sanitized" },
15684
+ absint: { context: "int", level: "validated" },
15685
+ intval: { context: "int", level: "validated" },
15686
+ sanitize_key: { context: "key", level: "sanitized" },
15687
+ sanitize_email: { context: "email", level: "validated" },
15688
+ sanitize_file_name: { context: "filename", level: "validated" },
15689
+ sanitize_url: { context: "url", level: "validated" },
15690
+ sanitize_title: { context: "slug", level: "validated" },
15691
+ sanitize_title_with_dashes: { context: "slug", level: "validated" },
15692
+ sanitize_user: { context: "username", level: "validated" },
15693
+ filter_var: { context: "validated", level: "validated" },
15694
+ wp_validate_boolean: { context: "validated", level: "validated" }
15695
+ };
15696
+ PSEUDO_SANITIZERS = new Set([
15697
+ "trim",
15698
+ "ltrim",
15699
+ "rtrim",
15700
+ "strtolower",
15701
+ "strtoupper",
15702
+ "ucfirst",
15703
+ "ucwords",
15704
+ "base64_encode",
15705
+ "base64_decode",
15706
+ "urldecode",
15707
+ "rawurldecode",
15708
+ "stripslashes",
15709
+ "html_entity_decode",
15710
+ "htmlspecialchars_decode",
15711
+ "json_decode",
15712
+ "unserialize",
15713
+ "serialize",
15714
+ "substr",
15715
+ "mb_substr",
15716
+ "explode",
15717
+ "implode",
15718
+ "str_replace",
15719
+ "preg_replace",
15720
+ "nl2br",
15721
+ "strval",
15722
+ "number_format",
15723
+ "sprintf",
15724
+ "md5",
15725
+ "sha1",
15726
+ "hash",
15727
+ "crypt",
15728
+ "chr",
15729
+ "ord",
15730
+ "strrev",
15731
+ "str_repeat",
15732
+ "str_pad"
15733
+ ]);
15734
+ SINK_REQUIREMENTS = {
15735
+ sql_execution: ["sql", "sql_param", "int"],
15736
+ code_execution: ["code"],
15737
+ command_execution: ["code"],
15738
+ deserialization: ["code"],
15739
+ file_operations: ["filename"],
15740
+ redirect: ["url"],
15741
+ html_render: ["html"]
15742
+ };
15743
+ SOURCE_TOKENS2 = /\$_(GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|->(input|query|request)\(|\breq\.(query|body|params|headers|cookies)|\bgetParameter\(/;
15596
15744
  });
15597
15745
 
15598
- // src/update.ts
15599
- async function checkLatestVersion(timeoutMs = 1e4) {
15600
- try {
15601
- const controller = new AbortController;
15602
- const t = setTimeout(() => controller.abort(), timeoutMs);
15603
- const res = await fetch("https://registry.npmjs.org/blitzstrike/latest", { signal: controller.signal });
15604
- clearTimeout(t);
15605
- if (!res.ok)
15606
- return null;
15607
- const data = await res.json();
15608
- return data.version ?? null;
15609
- } catch {
15610
- return null;
15611
- }
15746
+ // src/framework-security.ts
15747
+ function frameworkKnowledge(framework = "wordpress") {
15748
+ return FRAMEWORKS[framework.toLowerCase()] ?? null;
15612
15749
  }
15613
- function semverCompare(a, b) {
15614
- const pa = a.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
15615
- const pb = b.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
15616
- for (let i = 0;i < 3; i++) {
15617
- const x = pa[i] ?? 0;
15618
- const y = pb[i] ?? 0;
15619
- if (x !== y)
15620
- return x < y ? -1 : 1;
15750
+ function listFrameworks2() {
15751
+ const s = new Set;
15752
+ for (const k of Object.keys(FRAMEWORKS))
15753
+ s.add(FRAMEWORKS[k].framework);
15754
+ return [...s];
15755
+ }
15756
+ function lineNoOf(code, pos) {
15757
+ let n = 1;
15758
+ for (let i = 0;i < pos; i++)
15759
+ if (code.charCodeAt(i) === 10)
15760
+ n++;
15761
+ return n;
15762
+ }
15763
+ function functionBody(code, name) {
15764
+ const idx = code.search(new RegExp(`function\\s+${name}\\s*\\(`));
15765
+ if (idx === -1)
15766
+ return null;
15767
+ const open = code.indexOf("{", idx);
15768
+ if (open === -1)
15769
+ return null;
15770
+ let depth = 0;
15771
+ for (let i = open;i < code.length; i++) {
15772
+ if (code[i] === "{")
15773
+ depth++;
15774
+ else if (code[i] === "}") {
15775
+ depth--;
15776
+ if (depth === 0)
15777
+ return code.slice(open, i + 1);
15778
+ }
15621
15779
  }
15622
- return 0;
15780
+ return code.slice(open);
15623
15781
  }
15624
- async function runUpdate(currentVersion) {
15625
- const latest = await checkLatestVersion();
15626
- const outdated = latest !== null && semverCompare(currentVersion, latest) < 0;
15627
- return {
15628
- current: currentVersion,
15629
- latest,
15630
- outdated,
15631
- update_command: "npm install -g blitzstrike@latest # or: bun add -g blitzstrike@latest",
15632
- data_sync: syncData()
15633
- };
15782
+ function handlerName(expr) {
15783
+ const quoted = expr.match(/['"]([A-Za-z_][A-Za-z0-9_]*)['"]/g);
15784
+ if (quoted && quoted.length > 0)
15785
+ return quoted[quoted.length - 1].replace(/['"]/g, "");
15786
+ return null;
15634
15787
  }
15635
- var init_update = __esm(() => {
15636
- init_sync();
15637
- });
15638
-
15639
- // src/release.ts
15640
- import { readFileSync as readFileSync26, existsSync as existsSync12 } from "node:fs";
15641
- import { join as join22 } from "node:path";
15642
- import { fileURLToPath as fileURLToPath12 } from "node:url";
15643
- import { execSync as execSync2 } from "node:child_process";
15644
- function validateRelease() {
15645
- const checks = [];
15646
- checks.push({ name: "package.json version", ok: true, detail: VERSION });
15647
- try {
15648
- const changelog = readFileSync26(join22(ROOT8, "CHANGELOG.md"), "utf8");
15649
- const m = changelog.match(/^##\s+\[([0-9]+\.[0-9]+\.[0-9]+)\]/m);
15650
- const changelogVersion = m?.[1] ?? null;
15651
- checks.push({
15652
- name: "CHANGELOG version",
15653
- ok: changelogVersion === VERSION,
15654
- detail: changelogVersion ?? "no version header found"
15655
- });
15656
- } catch {
15657
- checks.push({ name: "CHANGELOG version", ok: false, detail: "CHANGELOG.md not found" });
15788
+ function detectWpAuthz(code, file, out) {
15789
+ const ajaxRe = /add_action\s*\(\s*['"](wp_ajax(?:_nopriv)?_\w+)['"]\s*,\s*(.+?)\s*\)/g;
15790
+ let m;
15791
+ while ((m = ajaxRe.exec(code)) !== null) {
15792
+ const hook = m[1];
15793
+ const handler = handlerName(m[2]);
15794
+ if (!handler)
15795
+ continue;
15796
+ const nopriv = hook.startsWith("wp_ajax_nopriv_");
15797
+ const body = functionBody(code, handler);
15798
+ if (!body)
15799
+ continue;
15800
+ const hasCap = AUTH_RE.test(body) || INDIRECT_AUTH.test(body);
15801
+ const hasNonce = NONCE_RE.test(body);
15802
+ const dangerous = DANGEROUS_OP_RE.test(body);
15803
+ const line = lineNoOf(code, m.index);
15804
+ if (nopriv && !hasCap && dangerous) {
15805
+ out.push({ file, line, type: "missing_authz", category: "Missing capability check (unauthenticated AJAX handler)", severity: "high", evidence: `add_action('${hook}', ...)`, detail: `Unauthenticated AJAX handler '${handler}()' performs a privileged/state-changing operation with no current_user_can()/authorization check — auth bypass / privilege escalation.` });
15806
+ }
15807
+ if (!hasNonce && dangerous) {
15808
+ out.push({ file, line, type: "missing_nonce", category: "Missing nonce verification", severity: "medium", evidence: `add_action('${hook}', ...)`, detail: `AJAX handler '${handler}()' performs a state-changing operation without nonce verification (wp_verify_nonce / check_ajax_referer) — CSRF.` });
15809
+ }
15658
15810
  }
15659
- try {
15660
- const tag = execSync2("git describe --tags --abbrev=0", { cwd: ROOT8, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
15661
- checks.push({ name: "Git tag", ok: tag === `v${VERSION}`, detail: tag, soft: true });
15662
- } catch {
15663
- checks.push({ name: "Git tag", ok: true, detail: "unavailable (no git/tags)", soft: true });
15811
+ const adminRe = /add_action\s*\(\s*['"](admin_post(?:_nopriv)?_\w+)['"]\s*,\s*(.+?)\s*\)/g;
15812
+ while ((m = adminRe.exec(code)) !== null) {
15813
+ const hook = m[1];
15814
+ const handler = handlerName(m[2]);
15815
+ if (!handler)
15816
+ continue;
15817
+ const nopriv = hook.startsWith("admin_post_nopriv_");
15818
+ const body = functionBody(code, handler);
15819
+ if (!body)
15820
+ continue;
15821
+ const hasCap = AUTH_RE.test(body) || INDIRECT_AUTH.test(body);
15822
+ const hasNonce = NONCE_RE.test(body);
15823
+ const dangerous = DANGEROUS_OP_RE.test(body);
15824
+ const line = lineNoOf(code, m.index);
15825
+ if (nopriv && !hasCap && dangerous) {
15826
+ out.push({ file, line, type: "missing_authz", category: "Missing capability check (unauthenticated admin-post handler)", severity: "high", evidence: `add_action('${hook}', ...)`, detail: `Unauthenticated admin-post handler '${handler}()' performs a privileged/state-changing operation with no authorization check — auth bypass / privilege escalation.` });
15827
+ }
15828
+ if (!hasNonce && dangerous) {
15829
+ out.push({ file, line, type: "missing_nonce", category: "Missing nonce verification", severity: "medium", evidence: `add_action('${hook}', ...)`, detail: `admin-post handler '${handler}()' performs a state-changing operation without nonce verification — CSRF.` });
15830
+ }
15664
15831
  }
15665
- try {
15666
- const published = execSync2("npm view blitzstrike version", { encoding: "utf8", timeout: 8000, stdio: ["ignore", "pipe", "ignore"] }).trim();
15667
- checks.push({ name: "npm registry version", ok: published === VERSION, detail: published, soft: true });
15668
- } catch {
15669
- checks.push({ name: "npm registry version", ok: true, detail: "unavailable (offline)", soft: true });
15832
+ const restRe = /register_rest_route\s*\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]/g;
15833
+ while ((m = restRe.exec(code)) !== null) {
15834
+ const line = lineNoOf(code, m.index);
15835
+ const window2 = code.slice(m.index, m.index + 800);
15836
+ const pc = window2.match(/permission_callback['"]?\s*=>\s*(.+?)(?:,|\s*\n|\])/);
15837
+ if (pc) {
15838
+ const expr = pc[1].trim();
15839
+ if (/__return_true/.test(expr)) {
15840
+ out.push({ file, line, type: "missing_authz", category: "REST route explicitly public (__return_true)", severity: "medium", evidence: `register_rest_route('${m[1]}', '${m[2]}')`, detail: `REST route '${m[1]}${m[2]}' sets permission_callback to __return_true (always allow) — verify the endpoint does not expose sensitive data or perform privileged actions.` });
15841
+ }
15842
+ continue;
15843
+ }
15844
+ out.push({ file, line, type: "missing_authz", category: "REST route missing permission_callback", severity: "high", evidence: `register_rest_route('${m[1]}', '${m[2]}')`, detail: `REST route '${m[1]}${m[2]}' has no permission_callback — the endpoint defaults to public (auth bypass).` });
15845
+ }
15846
+ const formRe = /add_action\s*\(\s*['"](init|admin_init|wp_loaded|template_redirect|parse_request|wp)['"]\s*,\s*(.+?)\s*\)/g;
15847
+ while ((m = formRe.exec(code)) !== null) {
15848
+ const hook = m[1];
15849
+ const handler = handlerName(m[2]);
15850
+ if (!handler)
15851
+ continue;
15852
+ const body = functionBody(code, handler);
15853
+ if (!body)
15854
+ continue;
15855
+ if (!/\$_(GET|POST|REQUEST|COOKIE|SERVER)\b|php:\/\/input|filter_input|file_get_contents\(['"]php:\/\/input/.test(body))
15856
+ continue;
15857
+ const hasCap = AUTH_RE.test(body) || INDIRECT_AUTH.test(body);
15858
+ const hasNonce = NONCE_RE.test(body);
15859
+ const dangerous = DANGEROUS_OP_RE.test(body);
15860
+ const line = lineNoOf(code, m.index);
15861
+ if (!hasNonce && dangerous) {
15862
+ out.push({ file, line, type: "missing_nonce", category: "Missing nonce verification (direct request handler)", severity: "medium", evidence: `add_action('${hook}', ...)`, detail: `Request handler '${handler}()' (on '${hook}') performs a state-changing operation without nonce verification — CSRF.` });
15863
+ }
15864
+ if (!hasCap && dangerous) {
15865
+ out.push({ file, line, type: "missing_authz", category: "Missing capability check (direct request handler)", severity: "high", evidence: `add_action('${hook}', ...)`, detail: `Request handler '${handler}()' (on '${hook}') performs a privileged/state-changing operation with no capability check — auth bypass / privilege escalation.` });
15866
+ }
15670
15867
  }
15671
- checks.push({
15672
- name: "Lockfile",
15673
- ok: lockfilePresent(),
15674
- detail: lockfilePresent() ? "present (bun.lock / package-lock.json)" : "missing",
15675
- soft: true
15676
- });
15677
- return { version: VERSION, checks, consistent: checks.filter((c) => !c.soft).every((c) => c.ok) };
15678
- }
15679
- function lockfilePresent() {
15680
- return existsSync12(join22(ROOT8, "package-lock.json")) || existsSync12(join22(ROOT8, "bun.lock")) || existsSync12(join22(ROOT8, "bun.lockb"));
15681
15868
  }
15682
- var ROOT8;
15683
- var init_release = __esm(() => {
15684
- init_version();
15685
- ROOT8 = join22(fileURLToPath12(new URL(".", import.meta.url)), "..");
15686
- });
15687
-
15688
- // src/dependency.ts
15689
- import { readFileSync as readFileSync27 } from "node:fs";
15690
- import { join as join23 } from "node:path";
15691
- import { fileURLToPath as fileURLToPath13 } from "node:url";
15692
- function checkDependencies() {
15693
- let pkg = {};
15694
- try {
15695
- pkg = JSON.parse(readFileSync27(join23(ROOT9, "package.json"), "utf8"));
15696
- } catch {
15697
- return { install_scripts: [], git_dependencies: [], total_dependencies: 0, safe: false };
15869
+ function detectRouteAuthz(code, file, out) {
15870
+ const ls = code.split(`
15871
+ `);
15872
+ for (const d of ROUTE_DETECTORS) {
15873
+ d.entry.lastIndex = 0;
15874
+ let m;
15875
+ while ((m = d.entry.exec(code)) !== null) {
15876
+ const ln = lineNoOf(code, m.index);
15877
+ const window2 = ls.slice(Math.max(0, ln - d.window), Math.min(ls.length, ln + d.window + 1)).join(`
15878
+ `);
15879
+ if (d.auth.test(window2))
15880
+ continue;
15881
+ out.push({
15882
+ file,
15883
+ line: ln,
15884
+ type: "missing_authz",
15885
+ category: `${d.framework}: endpoint without authorization`,
15886
+ severity: "high",
15887
+ evidence: (ls[ln - 1] ?? "").trim(),
15888
+ detail: `${d.framework} endpoint has no authorization enforcement in the surrounding ${d.window}-line window — auth bypass / privilege escalation.`
15889
+ });
15890
+ }
15698
15891
  }
15699
- const installScriptNames = ["preinstall", "install", "postinstall", "prepare"];
15700
- const installScripts = installScriptNames.filter((n) => pkg.scripts?.[n]).map((n) => ({ name: n, command: pkg.scripts[n] }));
15701
- const allDeps = {
15702
- ...pkg.dependencies ?? {},
15703
- ...pkg.devDependencies ?? {},
15704
- ...pkg.peerDependencies ?? {},
15705
- ...pkg.optionalDependencies ?? {}
15706
- };
15707
- const gitDependencies = Object.entries(allDeps).filter(([, v]) => /^(git\+|git:|github:|gitlab:|bitbucket:|\w+:\/\/)/.test(v)).map(([name, v]) => `${name}@${v}`);
15708
- const total = Object.keys(allDeps).length;
15709
- return {
15710
- install_scripts: installScripts,
15711
- git_dependencies: gitDependencies,
15712
- total_dependencies: total,
15713
- safe: installScripts.length === 0 && gitDependencies.length === 0
15714
- };
15715
15892
  }
15716
- var ROOT9;
15717
- var init_dependency = __esm(() => {
15718
- ROOT9 = join23(fileURLToPath13(new URL(".", import.meta.url)), "..");
15893
+ function detectMissingAuthz(code, file) {
15894
+ const out = [];
15895
+ detectWpAuthz(code, file, out);
15896
+ detectRouteAuthz(code, file, out);
15897
+ const seen = new Set;
15898
+ return out.filter((f) => {
15899
+ const key = `${f.type}:${f.line}`;
15900
+ if (seen.has(key))
15901
+ return false;
15902
+ seen.add(key);
15903
+ return true;
15904
+ });
15905
+ }
15906
+ var FRAMEWORKS, INDIRECT_AUTH, AUTH_RE, NONCE_RE, DANGEROUS_OP_RE, ROUTE_DETECTORS;
15907
+ var init_framework_security = __esm(() => {
15908
+ FRAMEWORKS = {
15909
+ wordpress: {
15910
+ framework: "wordpress",
15911
+ auth_primitives: ["current_user_can", "current_user_can_edit_post", "is_user_logged_in", "authorize"],
15912
+ nonce_primitives: ["wp_verify_nonce", "check_ajax_referer", "check_admin_referer", "verify_nonce"],
15913
+ entry_point_hooks: ["wp_ajax_", "wp_ajax_nopriv_", "admin_post_", "admin_post_nopriv_", "register_rest_route", "add_shortcode", "init", "rest_api_init"],
15914
+ sanitizers: ["sanitize_text_field", "esc_sql", "esc_html", "esc_attr", "esc_url", "wp_kses", "wp_kses_post", "absint", "sanitize_email", "sanitize_file_name"]
15915
+ },
15916
+ wp: {
15917
+ framework: "wordpress",
15918
+ auth_primitives: ["current_user_can", "is_user_logged_in", "authorize"],
15919
+ nonce_primitives: ["wp_verify_nonce", "check_ajax_referer", "check_admin_referer"],
15920
+ entry_point_hooks: ["wp_ajax_", "wp_ajax_nopriv_", "register_rest_route"],
15921
+ sanitizers: ["sanitize_text_field", "esc_sql", "esc_html", "absint"]
15922
+ },
15923
+ laravel: {
15924
+ framework: "laravel",
15925
+ auth_primitives: ["auth", "authorize", "Gate::", "can:", "->middleware('auth'", "->can(", "$this->authorize", "@can", "@cannot", "policy"],
15926
+ nonce_primitives: ["@csrf", "csrf_field", "VerifyCsrfToken"],
15927
+ entry_point_hooks: ["Route::get", "Route::post", "Route::put", "Route::delete", "Route::resource", "Route::match"],
15928
+ sanitizers: ["e(", "htmlspecialchars", "clean(", "strip_tags"]
15929
+ },
15930
+ django: {
15931
+ framework: "django",
15932
+ auth_primitives: ["login_required", "permission_required", "user_passes_test", "LoginRequiredMixin", "PermissionRequiredMixin", "permission_classes", "IsAuthenticated", "has_permission"],
15933
+ nonce_primitives: ["csrf_protect", "CsrfViewMiddleware", "@csrf"],
15934
+ entry_point_hooks: ["def ", "@api_view", "path(", "url(", "re_path("],
15935
+ sanitizers: ["escape(", "mark_safe", "html.escape"]
15936
+ },
15937
+ flask: {
15938
+ framework: "flask",
15939
+ auth_primitives: ["login_required", "@login_required", "current_user.is_authenticated", "permission_required"],
15940
+ nonce_primitives: ["CSRFProtect", "csrf_token"],
15941
+ entry_point_hooks: ["@app.route", "@blueprint.route", "@bp.route"],
15942
+ sanitizers: ["escape(", "Markup", "html.escape"]
15943
+ },
15944
+ express: {
15945
+ framework: "express",
15946
+ auth_primitives: ["requireAuth", "authenticate", "passport.authenticate", "req.user", "isAuthenticated", "ensureAuthenticated", "requireLogin", "authorize", "authMiddleware"],
15947
+ nonce_primitives: ["csurf", "csrfProtection", "helmet"],
15948
+ entry_point_hooks: ["app.get", "app.post", "router.get", "router.post"],
15949
+ sanitizers: ["escapeHtml", "encodeURIComponent", "sanitize", "xss"]
15950
+ },
15951
+ spring: {
15952
+ framework: "spring",
15953
+ auth_primitives: ["@PreAuthorize", "@Secured", "@RolesAllowed", "hasRole", "hasAuthority", "isAuthenticated", "SecurityContextHolder"],
15954
+ nonce_primitives: ["csrf", "CsrfToken"],
15955
+ entry_point_hooks: ["@GetMapping", "@PostMapping", "@RequestMapping", "@Controller"],
15956
+ sanitizers: ["HtmlUtils.htmlEscape", "escapeHtml", "StringEscapeUtils"]
15957
+ },
15958
+ symfony: {
15959
+ framework: "symfony",
15960
+ auth_primitives: ["is_granted", "denyAccessUnlessGranted", "->isGranted(", "IsGranted", "Voter"],
15961
+ nonce_primitives: ["isCsrfTokenValid", "csrf_token"],
15962
+ entry_point_hooks: ["#[Route", "@Route", "#[Get", "#[Post"],
15963
+ sanitizers: ["e(", "htmlspecialchars", "escape("]
15964
+ },
15965
+ aspnet: {
15966
+ framework: "aspnet",
15967
+ auth_primitives: ["[Authorize", "[AllowAnonymous", "User.Identity", "IsInRole", "AuthorizeAsync", "IAuthorizationService"],
15968
+ nonce_primitives: ["ValidateAntiForgeryToken", "Antiforgery"],
15969
+ entry_point_hooks: ["[HttpGet", "[HttpPost", "[Route("],
15970
+ sanitizers: ["HtmlEncoder", "Html.Raw", "UrlEncoder"]
15971
+ },
15972
+ rails: {
15973
+ framework: "rails",
15974
+ auth_primitives: ["authenticate_user!", "authorize", "before_action :authenticate", "before_action :authorize", "current_user"],
15975
+ nonce_primitives: ["protect_from_forgery", "form_authenticity_token"],
15976
+ entry_point_hooks: ["def ", "before_action"],
15977
+ sanitizers: ["h(", "html_escape", "sanitize"]
15978
+ }
15979
+ };
15980
+ INDIRECT_AUTH = /\b[a-zA-Z0-9_]*(auth|permission|cap|nonce|can_|check|verify|allow)[a-zA-Z0-9_]*\s*\(/;
15981
+ AUTH_RE = /current_user_can|is_user_logged_in|is_super_admin|current_user_can_edit_post|authorize/;
15982
+ NONCE_RE = /wp_verify_nonce|check_ajax_referer|check_admin_referer|verify_nonce/;
15983
+ DANGEROUS_OP_RE = /\b(update_option|add_option|delete_option|update_site_option|delete_site_option|update_post_meta|add_post_meta|delete_post_meta|update_user_meta|add_user_meta|delete_user_meta|update_metadata|add_metadata|delete_metadata|set_transient|delete_transient|wp_insert_post|wp_update_post|wp_delete_post|wp_trash_post|wp_untrash_post|wp_insert_comment|wp_update_comment|wp_delete_comment|wp_insert_user|wp_update_user|wp_delete_user|wp_create_user|wp_set_current_user|wp_set_auth_cookie|set_user_role|set_role|add_role|remove_role|add_cap|remove_cap|wp_mail|wp_new_user_notification|wp_handle_upload|move_uploaded_file|wp_upload_bits|wp_set_object_terms|wp_set_post_terms|wp_schedule_event|wp_clear_scheduled_hook|wp_redirect|file_put_contents|unlink|rename|wpdb->query|wpdb->insert|wpdb->update|wpdb->delete|wpdb->replace)\s*\(/;
15984
+ ROUTE_DETECTORS = [
15985
+ { framework: "laravel", entry: /Route::(?:get|post|put|patch|delete|any|match|resource)\s*\(\s*['"][^'"]+['"]/g, auth: /->middleware\s*\(\s*['"]auth|middleware\s*\(\s*\[[^\]]*['"]auth|->can\s*\(|\$this->authorize|Gate::|authorize\s*\(|@can\b/, window: 2 },
15986
+ { framework: "django", entry: /@api_view\s*\(|def\s+\w+\s*\(\s*request\s*[,)]/g, auth: /login_required|permission_required|user_passes_test|LoginRequiredMixin|PermissionRequiredMixin|permission_classes|IsAuthenticated|has_permission/, window: 3 },
15987
+ { framework: "flask", entry: /@(?:app|bp|blueprint)\.route\s*\(/g, auth: /login_required|current_user\.is_authenticated|permission_required/, window: 3 },
15988
+ { framework: "express", entry: /(?:app|router)\.(?:get|post|put|delete|patch|use)\s*\(\s*['"][^'"]+['"]/g, auth: /requireAuth|authenticate|passport\.authenticate|req\.user|isAuthenticated|ensureAuthenticated|requireLogin|\.authorize|authMiddleware/, window: 2 },
15989
+ { framework: "spring", entry: /@(?:Get|Post|Put|Delete|Patch|Request)Mapping\s*\(/g, auth: /@PreAuthorize|@Secured|@RolesAllowed|hasRole|hasAuthority|isAuthenticated|SecurityContextHolder/, window: 2 },
15990
+ { framework: "symfony", entry: /#\[(?:Route|Get|Post|Put|Delete|Patch)\b|@Route\s*\(/g, auth: /is_granted|denyAccessUnlessGranted|->isGranted\s*\(|IsGranted|Voter/, window: 2 },
15991
+ { framework: "aspnet", entry: /\[Http(?:Get|Post|Put|Delete|Patch)\b/g, auth: /\[Authorize|User\.Identity|IsInRole|AuthorizeAsync|\[AllowAnonymous|IAuthorizationService/, window: 2 },
15992
+ { framework: "rails", entry: /class\s+\w+\s*<\s*ApplicationController/g, auth: /before_action\s+:authenticate|before_action\s+:authorize|authenticate_user!|authorize\s+|current_user|authenticate/, window: 6 }
15993
+ ];
15719
15994
  });
15720
15995
 
15721
- // src/cli.ts
15722
- import { readFileSync as readFileSync28, existsSync as existsSync13, writeFileSync as writeFileSync9, mkdirSync as mkdirSync10 } from "node:fs";
15723
- import { join as join24, dirname as dirname4 } from "node:path";
15724
- import { homedir as homedir11 } from "node:os";
15725
- import { fileURLToPath as fileURLToPath14 } from "node:url";
15726
- import { spawnSync as spawnSync5 } from "node:child_process";
15727
- function which(cmd) {
15728
- return spawnSync5("sh", ["-c", 'command -v "$1" >/dev/null 2>&1', "sh", cmd]).status === 0;
15996
+ // src/complex-bugs.ts
15997
+ function lines2(code) {
15998
+ return code.split(`
15999
+ `);
15729
16000
  }
15730
- function installedCount() {
15731
- const tools = loadTools();
15732
- const installed = tools.filter((t) => which(t.name)).map((t) => t.name);
15733
- return { total: tools.length, installed, missing: tools.map((t) => t.name).filter((n) => !installed.includes(n)) };
16001
+ function lineNo2(code, pos) {
16002
+ let n = 1;
16003
+ for (let i = 0;i < pos; i++)
16004
+ if (code.charCodeAt(i) === 10)
16005
+ n++;
16006
+ return n;
15734
16007
  }
15735
- async function runDoctor() {
15736
- console.log(`BlitzStrike doctor — environment health check
15737
- `);
15738
- const issues = [];
15739
- const bunOk = which("bun") || which("node");
15740
- issues.push({
15741
- name: "Runtime (bun/node)",
16008
+ function stripCommentsOnly(code) {
16009
+ const out = code.split("");
16010
+ const n = code.length;
16011
+ let i = 0;
16012
+ while (i < n) {
16013
+ const c = code[i];
16014
+ const nc = code[i + 1];
16015
+ if (c === "'" || c === '"') {
16016
+ const q = c;
16017
+ i++;
16018
+ while (i < n && code[i] !== q) {
16019
+ if (code[i] === "\\")
16020
+ i++;
16021
+ i++;
16022
+ }
16023
+ i++;
16024
+ continue;
16025
+ }
16026
+ if (c === "/" && nc === "/" || c === "#") {
16027
+ while (i < n && code[i] !== `
16028
+ `) {
16029
+ out[i] = " ";
16030
+ i++;
16031
+ }
16032
+ continue;
16033
+ }
16034
+ if (c === "/" && nc === "*") {
16035
+ out[i] = " ";
16036
+ out[i + 1] = " ";
16037
+ i += 2;
16038
+ while (i < n && !(code[i] === "*" && code[i + 1] === "/")) {
16039
+ if (code[i] !== `
16040
+ `)
16041
+ out[i] = " ";
16042
+ i++;
16043
+ }
16044
+ if (i < n) {
16045
+ out[i] = " ";
16046
+ if (i + 1 < n)
16047
+ out[i + 1] = " ";
16048
+ i += 2;
16049
+ }
16050
+ continue;
16051
+ }
16052
+ i++;
16053
+ }
16054
+ return out.join("");
16055
+ }
16056
+ function detectDeserialization(code, file, out) {
16057
+ if (!MAGIC_METHODS.test(code))
16058
+ return;
16059
+ const ls = lines2(code);
16060
+ const re = /\b(unserialize|maybe_unserialize)\s*\(/g;
16061
+ let m;
16062
+ while ((m = re.exec(code)) !== null) {
16063
+ const ln = lineNo2(code, m.index);
16064
+ const l = ls[ln - 1] ?? "";
16065
+ const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
16066
+ `);
16067
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
16068
+ continue;
16069
+ out.push({
16070
+ file,
16071
+ line: ln,
16072
+ type: "deserialization",
16073
+ category: "Deserialization → POP gadget chain",
16074
+ severity: "high",
16075
+ evidence: l.trim(),
16076
+ detail: `\`${m[1]}()\` of attacker-controlled data AND a magic method is present — object injection can pivot to RCE via the gadget chain.`
16077
+ });
16078
+ }
16079
+ }
16080
+ function detectTypeJuggling(code, file, out) {
16081
+ const ls = lines2(code);
16082
+ const re = /(?<![=!<>])(==|!=)(?!=)/g;
16083
+ let m;
16084
+ while ((m = re.exec(code)) !== null) {
16085
+ const ln = lineNo2(code, m.index);
16086
+ const l = ls[ln - 1] ?? "";
16087
+ const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
16088
+ `);
16089
+ const hasSource = SOURCE_TOKENS3.test(l) || SOURCE_TOKENS3.test(scope);
16090
+ const hasHash = /\b(md5|sha1|sha256|hash|crc32|crypt|password_verify|strcmp|hash_equals|secret|token|nonce)\s*\(?/i.test(l) || /\b(secret|token|nonce|hash)\b/i.test(l);
16091
+ if (!hasSource || !hasHash)
16092
+ continue;
16093
+ out.push({
16094
+ file,
16095
+ line: ln,
16096
+ type: "type_juggling",
16097
+ category: "Type juggling (loose comparison)",
16098
+ severity: "medium",
16099
+ evidence: l.trim(),
16100
+ detail: 'Loose comparison of user input against a hash/secret — PHP type juggling (`0e…`, `"str" == 0`) may bypass the check. Use strict `===`.'
16101
+ });
16102
+ }
16103
+ }
16104
+ function detectMassAssignment(code, file, out) {
16105
+ const ls = lines2(code);
16106
+ const extractRe = /\bextract\s*\(\s*[^)]*(REQUEST|POST|GET|COOKIE|FILES|\$_(REQUEST|POST|GET|COOKIE|FILES))/g;
16107
+ let m;
16108
+ while ((m = extractRe.exec(code)) !== null) {
16109
+ const ln = lineNo2(code, m.index);
16110
+ const l = ls[ln - 1] ?? "";
16111
+ if (/EXTR_SKIP|EXTR_PREFIX_ALL/.test(l))
16112
+ continue;
16113
+ out.push({
16114
+ file,
16115
+ line: ln,
16116
+ type: "mass_assignment",
16117
+ category: "Mass assignment (variable injection)",
16118
+ severity: "high",
16119
+ evidence: l.trim(),
16120
+ detail: "`extract()` of request data without EXTR_SKIP — attacker can overwrite arbitrary variables."
16121
+ });
16122
+ }
16123
+ const parseRe = /\bparse_str\s*\(/g;
16124
+ while ((m = parseRe.exec(code)) !== null) {
16125
+ const ln = lineNo2(code, m.index);
16126
+ const l = ls[ln - 1] ?? "";
16127
+ const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
16128
+ `);
16129
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
16130
+ continue;
16131
+ out.push({
16132
+ file,
16133
+ line: ln,
16134
+ type: "mass_assignment",
16135
+ category: "Mass assignment (variable injection)",
16136
+ severity: "medium",
16137
+ evidence: l.trim(),
16138
+ detail: "`parse_str()` of user input into the symbol table — variable injection."
16139
+ });
16140
+ }
16141
+ }
16142
+ function detectComplexBugs(code, file) {
16143
+ const out = [];
16144
+ detectDeserialization(code, file, out);
16145
+ detectTypeJuggling(code, file, out);
16146
+ detectMassAssignment(code, file, out);
16147
+ detectPrototypePollution(code, file, out);
16148
+ detectCrlfInjection(code, file, out);
16149
+ detectPathConfusion(code, file, out);
16150
+ detectSsrF(code, file, out);
16151
+ detectXxe(code, file, out);
16152
+ detectSsti(code, file, out);
16153
+ detectFileUploadRce(code, file, out);
16154
+ detectUnrestrictedUpload(code, file, out);
16155
+ detectSqlInjection(code, file, out);
16156
+ detectHardcodedSecret(code, file, out);
16157
+ detectLlmInjection(code, file, out);
16158
+ detectGraphqlExposure(code, file, out);
16159
+ detectOauthMisconfig(code, file, out);
16160
+ detectGrpcReflection(code, file, out);
16161
+ out.push(...detectWrongSanitizer(code, file));
16162
+ out.push(...detectMissingAuthz(code, file));
16163
+ const seen = new Set;
16164
+ return out.filter((f) => {
16165
+ const key = `${f.type}:${f.line}`;
16166
+ if (seen.has(key))
16167
+ return false;
16168
+ seen.add(key);
16169
+ return true;
16170
+ });
16171
+ }
16172
+ function detectPrototypePollution(code, file, out) {
16173
+ const ls = lines2(code);
16174
+ const mergeRe = /\b(Object\.assign|_\.merge|_\.defaultsDeep|\.extend|\.merge|\.defaultsDeep|mergeDeep|deepMerge|_.set)\s*\(/g;
16175
+ let m;
16176
+ while ((m = mergeRe.exec(code)) !== null) {
16177
+ const ln = lineNo2(code, m.index);
16178
+ const l = ls[ln - 1] ?? "";
16179
+ const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
16180
+ `);
16181
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
16182
+ continue;
16183
+ out.push({
16184
+ file,
16185
+ line: ln,
16186
+ type: "prototype_pollution",
16187
+ category: "Prototype pollution (unsafe merge)",
16188
+ severity: "high",
16189
+ evidence: l.trim(),
16190
+ detail: "Merge/extend of request data into an object without a `__proto__`/`constructor.prototype` guard — attacker-controlled keys can pollute `Object.prototype` and pivot to RCE or property injection."
16191
+ });
16192
+ }
16193
+ }
16194
+ function detectCrlfInjection(code, file, out) {
16195
+ const ls = lines2(code);
16196
+ const headerRe = /\b(header|setHeader|addHeader|writeHead|appendHeader)\s*\(/g;
16197
+ let m;
16198
+ while ((m = headerRe.exec(code)) !== null) {
16199
+ const ln = lineNo2(code, m.index);
16200
+ const l = ls[ln - 1] ?? "";
16201
+ const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
16202
+ `);
16203
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
16204
+ continue;
16205
+ out.push({
16206
+ file,
16207
+ line: ln,
16208
+ type: "crlf_injection",
16209
+ category: "CRLF / header injection",
16210
+ severity: "medium",
16211
+ evidence: l.trim(),
16212
+ detail: "User input reaches an HTTP header — CRLF (`%0d%0a`) can split the response, inject headers, or poison cookies. Strip newlines from header values."
16213
+ });
16214
+ }
16215
+ }
16216
+ function detectPathConfusion(code, file, out) {
16217
+ const ls = lines2(code);
16218
+ const fileRe = /\b(file_get_contents|file_put_contents|fopen|readFileSync|readFile|open|include|require|new\s+File|Files\.read|FileInputStream|FileOutputStream)\s*\(/g;
16219
+ let m;
16220
+ while ((m = fileRe.exec(code)) !== null) {
16221
+ const ln = lineNo2(code, m.index);
16222
+ const l = ls[ln - 1] ?? "";
16223
+ const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
16224
+ `);
16225
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
16226
+ continue;
16227
+ if (/\b(basename|realpath|canonicalPath|getCanonicalPath|normalize|resolve|toRealPath)\s*\(/i.test(scope + `
16228
+ ` + l))
16229
+ continue;
16230
+ if (/\$[A-Za-z_]\w*\s*\[\s*\$[A-Za-z_]\w*\s*\]/.test(scope + `
16231
+ ` + l))
16232
+ continue;
16233
+ if (/["'][^"']*:\/\/[^"']*["']/.test(scope + `
16234
+ ` + l))
16235
+ continue;
16236
+ out.push({
16237
+ file,
16238
+ line: ln,
16239
+ type: "path_confusion",
16240
+ category: "Path confusion (unsanitized file path)",
16241
+ severity: "medium",
16242
+ evidence: l.trim(),
16243
+ detail: "User input reaches a file path with no canonicalization (`basename`/`realpath`/`normalize`) — traversal, `..%2f`, backslash and null-byte variants can confuse a naive `../` filter."
16244
+ });
16245
+ }
16246
+ }
16247
+ function detectSsrF(code, file, out) {
16248
+ const ls = lines2(code);
16249
+ const re = /\b(file_get_contents|fopen|file|readfile|curl_setopt|curl_init|fetch|axios|requests\.(get|post|put)|urllib\.request|http_get|wp_remote_(get|post)|new\s+URL\s*\(|got\(|http\.(get|request))\s*\(/g;
16250
+ let m;
16251
+ while ((m = re.exec(code)) !== null) {
16252
+ const ln = lineNo2(code, m.index);
16253
+ const l = ls[ln - 1] ?? "";
16254
+ const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
16255
+ `);
16256
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
16257
+ continue;
16258
+ if (/(allowlist|whitelist|in_array|preg_match|parse_url|startsWith\(["']https?|endsWith\(["']|basename|realpath|normalize)/i.test(scope + `
16259
+ ` + l))
16260
+ continue;
16261
+ if (/["'][^"']*:\/\/[^"']*["']/.test(scope + `
16262
+ ` + l))
16263
+ continue;
16264
+ out.push({
16265
+ file,
16266
+ line: ln,
16267
+ type: "ssrf",
16268
+ category: "SSRF (user-controlled URL)",
16269
+ severity: "high",
16270
+ evidence: l.trim(),
16271
+ detail: "User input reaches a URL-accepting sink with no host allowlist — SSRF (cloud metadata, internal port scan, IAM credential theft)."
16272
+ });
16273
+ }
16274
+ }
16275
+ function detectXxe(code, file, out) {
16276
+ const ls = lines2(code);
16277
+ const re = /\b(simplexml_load_string|simplexml_load_file|new\s+SimpleXMLElement|->loadXML\s*\(|->load\s*\(|XMLReader\s*->\s*XML\s*\(|libxml_parse)\s*\(?/g;
16278
+ let m;
16279
+ while ((m = re.exec(code)) !== null) {
16280
+ const ln = lineNo2(code, m.index);
16281
+ const l = ls[ln - 1] ?? "";
16282
+ const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
16283
+ `);
16284
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
16285
+ continue;
16286
+ if (/libxml_disable_entity_loader\s*\(\s*true|LIBXML_NONET/.test(scope + `
16287
+ ` + l))
16288
+ continue;
16289
+ out.push({
16290
+ file,
16291
+ line: ln,
16292
+ type: "xxe",
16293
+ category: "XXE (unprotected XML parse)",
16294
+ severity: "high",
16295
+ evidence: l.trim(),
16296
+ detail: "User-controlled XML parsed without disabling external entities — XXE (file read, SSRF, billion-laughs DoS)."
16297
+ });
16298
+ }
16299
+ }
16300
+ function detectSsti(code, file, out) {
16301
+ const ls = lines2(code);
16302
+ const re = /\b(render_template_string|Template\s*\(|res\.render\s*\(|ejs\.(render|compile)\s*\(|_\.template\s*\(|render_string|twig\.render|Jinja2\(|jinja2\.Template)\s*\(?/g;
16303
+ let m;
16304
+ while ((m = re.exec(code)) !== null) {
16305
+ const ln = lineNo2(code, m.index);
16306
+ const l = ls[ln - 1] ?? "";
16307
+ const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
16308
+ `);
16309
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
16310
+ continue;
16311
+ out.push({
16312
+ file,
16313
+ line: ln,
16314
+ type: "ssti",
16315
+ category: "SSTI (template render of user input)",
16316
+ severity: "high",
16317
+ evidence: l.trim(),
16318
+ detail: "User input reaches a template render with no sandbox — SSTI (`{{7*7}}`, `${7*7}`, `#{7*7}`) → RCE."
16319
+ });
16320
+ }
16321
+ }
16322
+ function detectFileUploadRce(code, file, out) {
16323
+ code = stripCommentsOnly(code);
16324
+ const ls = lines2(code);
16325
+ const re = /\b(move_uploaded_file|wp_upload_bits|->move\s*\(|storeAs\s*\(|->storeAs\s*\(|putFile\s*\()\s*\(/g;
16326
+ let m;
16327
+ while ((m = re.exec(code)) !== null) {
16328
+ const ln = lineNo2(code, m.index);
16329
+ const l = ls[ln - 1] ?? "";
16330
+ const scope = ls.slice(Math.max(0, ln - 30), ln + 3).join(`
16331
+ `);
16332
+ if (!/\$_FILES\b|tmp_name|getUploadedFile|uploadedFile|\['name'\]/i.test(scope))
16333
+ continue;
16334
+ if (/(wp_check_filetype|PATHINFO_EXTENSION|wp_get_mime_types|allowed_?(types|extensions|exts)|mime_?(types|extensions)|in_array\s*\([^)]*\$(?:allowed|whitelist|ext)|wp_handle_upload\s*\(\s*[^)]*test_form\s*=>\s*true)/i.test(scope))
16335
+ continue;
16336
+ out.push({
16337
+ file,
16338
+ line: ln,
16339
+ type: "file_upload",
16340
+ category: "File upload → RCE (unvalidated extension)",
16341
+ severity: "high",
16342
+ evidence: l.trim(),
16343
+ detail: "An uploaded file is written with an attacker-controlled name and no visible extension/filetype validation — upload a .php/.phtml shell → RCE."
16344
+ });
16345
+ }
16346
+ }
16347
+ function detectUnrestrictedUpload(code, file, out) {
16348
+ const ls = lines2(code);
16349
+ const re = /['"]uploadAllow['"]\s*=>\s*(?:array\s*\(\s*)?['"]all['"]/g;
16350
+ let m;
16351
+ while ((m = re.exec(code)) !== null) {
16352
+ const ln = lineNo2(code, m.index);
16353
+ const l = ls[ln - 1] ?? "";
16354
+ const scope = ls.slice(Math.max(0, ln - 4), ln + 4).join(`
16355
+ `);
16356
+ const denyFirst = /['"]uploadOrder['"]\s*=>\s*array\s*\(\s*['"]deny['"]/.test(scope);
16357
+ out.push({
16358
+ file,
16359
+ line: ln,
16360
+ type: "file_upload",
16361
+ category: "Unrestricted upload config (uploadAllow=all)",
16362
+ severity: denyFirst ? "high" : "medium",
16363
+ evidence: l.trim(),
16364
+ detail: denyFirst ? "File-manager/connector config sets uploadAllow='all' with uploadOrder deny→allow — every mimetype (including .php) is uploadable → RCE." : "File-manager/connector config sets uploadAllow='all' — any file type can be uploaded; verify the upload path is not web-reachable or that a server-side extension whitelist exists."
16365
+ });
16366
+ }
16367
+ }
16368
+ function detectSqlInjection(code, file, out) {
16369
+ const ls = lines2(code);
16370
+ const sinkRe = /\$wpdb->(get_results|get_col|get_var|get_row|query)\s*\(|->query\s*\(|DB::unprepared\s*\(|->whereRaw\s*\(|->selectRaw\s*\(/g;
16371
+ let m;
16372
+ while ((m = sinkRe.exec(code)) !== null) {
16373
+ const ln = lineNo2(code, m.index);
16374
+ const l = ls[ln - 1] ?? "";
16375
+ const scope = ls.slice(Math.max(0, ln - 6), ln + 1).join(`
16376
+ `);
16377
+ if (/->prepare\s*\(/.test(scope))
16378
+ continue;
16379
+ if (SOURCE_TOKENS3.test(scope))
16380
+ continue;
16381
+ const inlineConcat = /\.\s*\$[A-Za-z_]\w*/.test(l);
16382
+ const assignConcat = /(?:^|\n)\s*\$[A-Za-z_]\w*\s*(?:\.=|=)\s*[^;\n]*["'][^"']*["']\s*\.\s*\$[A-Za-z_]\w*/.test(scope);
16383
+ if (!inlineConcat && !assignConcat)
16384
+ continue;
16385
+ out.push({
16386
+ file,
16387
+ line: ln,
16388
+ type: "sql_injection",
16389
+ category: "SQL injection (raw string-interpolated query)",
16390
+ severity: "medium",
16391
+ evidence: l.trim(),
16392
+ detail: "A SQL sink receives a query built by string concatenation (not prepared), with no visible source in this file — trace the concatenated variable back to a request source; if user-controlled, this is SQL injection (CWE-89)."
16393
+ });
16394
+ }
16395
+ }
16396
+ function detectHardcodedSecret(code, file, out) {
16397
+ const ls = lines2(code);
16398
+ const clean = stripCommentsOnly(code);
16399
+ const flaggedLines = new Set;
16400
+ SECRET_FIELD_RE.lastIndex = 0;
16401
+ let m;
16402
+ while ((m = SECRET_FIELD_RE.exec(clean)) !== null) {
16403
+ const field = m[1];
16404
+ const rest = clean.slice(m.index + m[0].length);
16405
+ const lineEnd = rest.search(/\r?\n/);
16406
+ const valueRaw = (lineEnd === -1 ? rest : rest.slice(0, lineEnd)).trim();
16407
+ let value = "";
16408
+ const qm = valueRaw.match(/^["']([^"']{4,})["']/);
16409
+ if (qm)
16410
+ value = qm[1];
16411
+ else
16412
+ value = (valueRaw.match(/^([A-Za-z0-9._/+=@-]{8,})/) ?? [])[1] ?? "";
16413
+ if (!value)
16414
+ continue;
16415
+ if (SECRET_PLACEHOLDER_RE.test(value))
16416
+ continue;
16417
+ if (/\$[A-Za-z_]|[{}\[\]]|process\.env|getenv|os\.environ|config\s*\(|env\s*\(/.test(valueRaw))
16418
+ continue;
16419
+ const ln = lineNo2(code, m.index);
16420
+ flaggedLines.add(ln);
16421
+ out.push({
16422
+ file,
16423
+ line: ln,
16424
+ type: "hardcoded_secret",
16425
+ category: "Hardcoded secret (credential in source)",
16426
+ severity: "high",
16427
+ evidence: (ls[ln - 1] ?? "").trim(),
16428
+ detail: `Secret field '${field}' is assigned a hardcoded value in source — the credential is exposed to anyone with code access (CWE-798 Hard-coded Credentials). Rotate it and load from the environment instead.`
16429
+ });
16430
+ }
16431
+ for (const fmt of SECRET_FORMATS) {
16432
+ fmt.re.lastIndex = 0;
16433
+ let fm;
16434
+ while ((fm = fmt.re.exec(clean)) !== null) {
16435
+ const ln = lineNo2(code, fm.index);
16436
+ if (flaggedLines.has(ln))
16437
+ continue;
16438
+ flaggedLines.add(ln);
16439
+ out.push({
16440
+ file,
16441
+ line: ln,
16442
+ type: "hardcoded_secret",
16443
+ category: "Hardcoded secret (known format)",
16444
+ severity: "high",
16445
+ evidence: (ls[ln - 1] ?? "").trim(),
16446
+ detail: `A ${fmt.name} was found hardcoded in source — the credential is exposed to anyone with code access (CWE-798). Rotate it and load from the environment instead.`
16447
+ });
16448
+ }
16449
+ }
16450
+ const isMinified = /\.min\.(js|css)$/i.test(file) || /[\\/](bundle|vendor|dist|build)[\\/]/i.test(file);
16451
+ if (!isMinified) {
16452
+ const entropyRe = /(\b[A-Za-z_]\w*)\s*(?:::|=>|=|:)\s*["']([A-Za-z0-9_-]{32,})["']/g;
16453
+ entropyRe.lastIndex = 0;
16454
+ let em;
16455
+ while ((em = entropyRe.exec(clean)) !== null) {
16456
+ const varName = em[1];
16457
+ const value = em[2];
16458
+ const ln = lineNo2(code, em.index);
16459
+ if (flaggedLines.has(ln))
16460
+ continue;
16461
+ if (ENTROPY_EXCLUDE_VAR.test(varName))
16462
+ continue;
16463
+ if (!/[A-Z]/.test(value) || !/[a-z]/.test(value) || !/[0-9]/.test(value))
16464
+ continue;
16465
+ if (shannonEntropy(value) < 4.5)
16466
+ continue;
16467
+ flaggedLines.add(ln);
16468
+ out.push({
16469
+ file,
16470
+ line: ln,
16471
+ type: "hardcoded_secret",
16472
+ category: "Suspected secret (high-entropy token)",
16473
+ severity: "medium",
16474
+ evidence: (ls[ln - 1] ?? "").trim(),
16475
+ detail: `'${varName}' is assigned a high-entropy token (Shannon ${shannonEntropy(value).toFixed(2)} bits/char) that looks like a generated credential — verify it is not a secret key/token (CWE-798).`
16476
+ });
16477
+ }
16478
+ }
16479
+ }
16480
+ function detectLlmInjection(code, file, out) {
16481
+ const ls = lines2(code);
16482
+ const clean = stripCommentsOnly(code);
16483
+ const llmCallRe = /\b(?:openai|anthropic|langchain|llama_index)\.[A-Za-z_.]+\s*\(|\b(?:ChatCompletion|completion|messages)\.create\s*\(|\bllm\.invoke\s*\(|\.chat\.completions\.create\s*\(/g;
16484
+ let m;
16485
+ while ((m = llmCallRe.exec(clean)) !== null) {
16486
+ const ln = lineNo2(code, m.index);
16487
+ const scope = ls.slice(Math.max(0, ln - 6), ln + 3).join(`
16488
+ `);
16489
+ if (!SOURCE_TOKENS3.test(scope))
16490
+ continue;
16491
+ out.push({
16492
+ file,
16493
+ line: ln,
16494
+ type: "llm_injection",
16495
+ category: "LLM prompt injection surface",
16496
+ severity: "medium",
16497
+ evidence: (ls[ln - 1] ?? "").trim(),
16498
+ detail: "Request input reaches an LLM call (chat/completion/invoke) with no visible prompt-injection boundary — untrusted text may override instructions or exfiltrate context (OWASP LLM01 Prompt Injection). Verify the prompt is sandboxed and output is not used for privileged actions."
16499
+ });
16500
+ }
16501
+ }
16502
+ function detectGraphqlExposure(code, file, out) {
16503
+ const ls = lines2(code);
16504
+ const clean = stripCommentsOnly(code);
16505
+ const re = /\b(introspection|playground|graphiql)\s*[:=]\s*(true|1)\b/gi;
16506
+ let m;
16507
+ while ((m = re.exec(clean)) !== null) {
16508
+ const ln = lineNo2(code, m.index);
16509
+ out.push({
16510
+ file,
16511
+ line: ln,
16512
+ type: "graphql_exposure",
16513
+ category: "GraphQL introspection/playground exposed",
16514
+ severity: "medium",
16515
+ evidence: (ls[ln - 1] ?? "").trim(),
16516
+ detail: `GraphQL ${m[1].toLowerCase()} is enabled (${m[2]}) in a configuration that appears production-reachable — introspection exposes the full schema/fields, and a playground enables interactive querying (CWE-200 Information Exposure). Disable in production.`
16517
+ });
16518
+ }
16519
+ }
16520
+ function detectOauthMisconfig(code, file, out) {
16521
+ const ls = lines2(code);
16522
+ const clean = stripCommentsOnly(code);
16523
+ const re = /\b(redirect_uri|redirectUrl|return_url|returnUrl|callback_url)\s*[:=]\s*/gi;
16524
+ let m;
16525
+ while ((m = re.exec(clean)) !== null) {
16526
+ const ln = lineNo2(code, m.index);
16527
+ const rest = clean.slice(m.index + m[0].length);
16528
+ const lineEnd = rest.search(/\r?\n/);
16529
+ const line = lineEnd === -1 ? rest : rest.slice(0, lineEnd);
16530
+ if (!/(request|params|query|\$_(?:GET|POST|REQUEST)|req\.)/i.test(line))
16531
+ continue;
16532
+ const scope = ls.slice(Math.max(0, ln - 6), ln + 3).join(`
16533
+ `);
16534
+ if (/(allowlist|whitelist|allowed_|in_array|startsWith|===\s*['"])/.test(scope))
16535
+ continue;
16536
+ out.push({
16537
+ file,
16538
+ line: ln,
16539
+ type: "oauth_misconfig",
16540
+ category: "OAuth redirect_uri misconfiguration",
16541
+ severity: "high",
16542
+ evidence: (ls[ln - 1] ?? "").trim(),
16543
+ detail: "An OAuth redirect_uri/callback is fed by request input with no allowlist validation — an attacker can redirect the authorization code/token to an attacker-controlled endpoint (CWE-601 Open Redirect → token theft). Validate the redirect target against a static allowlist."
16544
+ });
16545
+ }
16546
+ }
16547
+ function detectGrpcReflection(code, file, out) {
16548
+ const ls = lines2(code);
16549
+ const clean = stripCommentsOnly(code);
16550
+ const re = /\b(enable_server_reflection|RegisterReflectionService|reflection\.enable|grpc_reflection|reflection\.Register)\b/gi;
16551
+ let m;
16552
+ while ((m = re.exec(clean)) !== null) {
16553
+ const ln = lineNo2(code, m.index);
16554
+ out.push({
16555
+ file,
16556
+ line: ln,
16557
+ type: "grpc_reflection",
16558
+ category: "gRPC reflection exposed",
16559
+ severity: "low",
16560
+ evidence: (ls[ln - 1] ?? "").trim(),
16561
+ detail: "gRPC server reflection is enabled — it enumerates every service/method to any caller, expanding the attack surface and leaking the API contract (CWE-200 Information Exposure). Disable reflection in production."
16562
+ });
16563
+ }
16564
+ }
16565
+ var SOURCE_TOKENS3, MAGIC_METHODS, SECRET_FIELD_RE, SECRET_PLACEHOLDER_RE, ENTROPY_EXCLUDE_VAR;
16566
+ var init_complex_bugs = __esm(() => {
16567
+ init_security_state();
16568
+ init_framework_security();
16569
+ init_secrets();
16570
+ SOURCE_TOKENS3 = /\$_(GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|\$request->|\$req->|request\.(get|input|query|post|data|body|headers|cookies|params)|getParameter\(|req\.(query|body|params|headers|cookies)/;
16571
+ MAGIC_METHODS = /\b(__destruct|__wakeup|__toString|__invoke|__call|__get|__set|__isset|__unset|__sleep)\s*\(/;
16572
+ SECRET_FIELD_RE = new RegExp(`\\b(${SECRET_FIELDS})\\s*(?:::|=>|=|:)\\s*`, "gi");
16573
+ SECRET_PLACEHOLDER_RE = /^(null|true|false|undefined|your_|xxx+|changeme|example|placeholder|test|secret|password|token|key|api|smtp|localhost|root|admin|pass|change|me|none|na|foo|bar|dummy)$/i;
16574
+ ENTROPY_EXCLUDE_VAR = /\b(data|payload|content|body|base64|encoded|encode|image|binary|blob|hash|digest|checksum|nonce|uuid|etag|signature|sig|cipher|iv|ciphertext|plaintext|thumb|thumbnail)\b/i;
16575
+ });
16576
+
16577
+ // src/verify-harness.ts
16578
+ function runVerificationHarness() {
16579
+ const cases = CASES.map((c) => {
16580
+ const pos = detectComplexBugs(c.positive, "positive.php").filter((f) => f.type === c.detector);
16581
+ const neg = detectComplexBugs(c.negative, "negative.php").filter((f) => f.type === c.detector);
16582
+ const positive_fired = pos.length > 0;
16583
+ const negative_fired = neg.length > 0;
16584
+ const tp = positive_fired;
16585
+ const tn = !negative_fired;
16586
+ const status = tp && tn ? "pass" : !tp ? "fn" : "fp";
16587
+ return { id: c.id, detector: c.detector, positive_fired, negative_fired, tp, tn, status };
16588
+ });
16589
+ const passed = cases.filter((c) => c.status === "pass").length;
16590
+ return {
16591
+ total: cases.length,
16592
+ passed,
16593
+ failed: cases.length - passed,
16594
+ tp_count: cases.filter((c) => c.tp).length,
16595
+ fp_count: cases.filter((c) => c.status === "fp").length,
16596
+ fn_count: cases.filter((c) => c.status === "fn").length,
16597
+ cases
16598
+ };
16599
+ }
16600
+ function verificationReportText() {
16601
+ const r = runVerificationHarness();
16602
+ const lines = [];
16603
+ lines.push(`Blitz Strike verification harness`);
16604
+ lines.push(` ${r.passed}/${r.total} detectors pass (TP + TN) · TP ${r.tp_count} · FP ${r.fp_count} · FN ${r.fn_count}`);
16605
+ lines.push(``);
16606
+ for (const c of r.cases) {
16607
+ const mark = c.status === "pass" ? "OK " : c.status === "fn" ? "FN " : "FP ";
16608
+ lines.push(` [${mark}] ${c.id.padEnd(20)} pos=${c.positive_fired ? "hit" : "miss"} neg=${c.negative_fired ? "HIT" : "clean"}`);
16609
+ }
16610
+ if (r.failed > 0) {
16611
+ lines.push(``);
16612
+ lines.push(` ${r.failed} detector(s) failed — see the cases above (FN = missed a positive, FP = fired on clean code).`);
16613
+ }
16614
+ return lines.join(`
16615
+ `);
16616
+ }
16617
+ var PHP = (body) => `<?php
16618
+ ${body}
16619
+ `, CASES;
16620
+ var init_verify_harness = __esm(() => {
16621
+ init_complex_bugs();
16622
+ CASES = [
16623
+ {
16624
+ id: "deserialization",
16625
+ detector: "deserialization",
16626
+ positive: PHP(`$o = unserialize($_GET['data']);
16627
+ class A { function __destruct() { system($this->c); } }`),
16628
+ negative: PHP(`$o = json_decode($_GET['data'], true);`)
16629
+ },
16630
+ {
16631
+ id: "type-juggling",
16632
+ detector: "type_juggling",
16633
+ positive: PHP(`$hash = "0e1234567890";
16634
+ if ($_GET['pwd'] == $hash) { login(); }`),
16635
+ negative: PHP(`if (hash_equals($_GET['pwd'], $hash)) { login(); }`)
16636
+ },
16637
+ {
16638
+ id: "mass-assignment",
16639
+ detector: "mass_assignment",
16640
+ positive: PHP(`extract($_POST);`),
16641
+ negative: PHP(`extract($data, EXTR_SKIP);`)
16642
+ },
16643
+ {
16644
+ id: "prototype-pollution",
16645
+ detector: "prototype_pollution",
16646
+ positive: PHP(`const merged = deepMerge({}, JSON.parse(req.body));`),
16647
+ negative: PHP(`const merged = Object.assign({}, { a: 1 });`)
16648
+ },
16649
+ {
16650
+ id: "crlf-injection",
16651
+ detector: "crlf_injection",
16652
+ positive: PHP(`header("Location: " . $_GET['url']);`),
16653
+ negative: PHP(`header("Location: /fixed");`)
16654
+ },
16655
+ {
16656
+ id: "path-traversal",
16657
+ detector: "path_confusion",
16658
+ positive: PHP(`include($_GET['page'] . ".php");`),
16659
+ negative: PHP(`include('views/' . basename($_GET['page']) . '.php');`)
16660
+ },
16661
+ {
16662
+ id: "ssrf",
16663
+ detector: "ssrf",
16664
+ positive: PHP(`$r = file_get_contents($_GET['url']);`),
16665
+ negative: PHP(`$r = file_get_contents('https://api.fixed.example/x');`)
16666
+ },
16667
+ {
16668
+ id: "xxe",
16669
+ detector: "xxe",
16670
+ positive: PHP(`$x = simplexml_load_string($_POST['xml']);`),
16671
+ negative: PHP(`$x = json_decode($_POST['data']);`)
16672
+ },
16673
+ {
16674
+ id: "ssti",
16675
+ detector: "ssti",
16676
+ positive: PHP(`return render_template_string($_GET['tpl']);`),
16677
+ negative: PHP(`return render_template('fixed.html');`)
16678
+ },
16679
+ {
16680
+ id: "wrong-sanitizer",
16681
+ detector: "wrong_sanitizer",
16682
+ positive: PHP(`$x = htmlspecialchars($_GET['q']);
16683
+ $wpdb->query("SELECT * FROM t WHERE c='" . $x . "'");`),
16684
+ negative: PHP(`$x = $wpdb->prepare("SELECT * FROM t WHERE c=%s", $_GET['q']);`)
16685
+ },
16686
+ {
16687
+ id: "missing-authz",
16688
+ detector: "missing_authz",
16689
+ positive: PHP(`add_action('wp_ajax_nopriv_set_opt', 'h');
16690
+ function h() { update_option('x', $_POST['x']); }`),
16691
+ negative: PHP(`add_action('wp_ajax_set_opt', 'h');
16692
+ function h() { if (!current_user_can('manage_options')) wp_die(); update_option('x', $_POST['x']); }`)
16693
+ },
16694
+ {
16695
+ id: "missing-nonce",
16696
+ detector: "missing_nonce",
16697
+ positive: PHP(`add_action('wp_ajax_del', 'h');
16698
+ function h() { wp_delete_post((int)$_POST['id']); }`),
16699
+ negative: PHP(`add_action('wp_ajax_del', 'h');
16700
+ function h() { check_ajax_referer('n'); wp_delete_post((int)$_POST['id']); }`)
16701
+ },
16702
+ {
16703
+ id: "file-upload",
16704
+ detector: "file_upload",
16705
+ positive: PHP(`$f = $_FILES['up'];
16706
+ move_uploaded_file($f['tmp_name'], '/uploads/' . $f['name']);`),
16707
+ negative: PHP(`$f = $_FILES['up'];
16708
+ if (in_array(pathinfo($f['name'], PATHINFO_EXTENSION), ['jpg','png'])) move_uploaded_file($f['tmp_name'], '/uploads/' . $f['name']);`)
16709
+ },
16710
+ {
16711
+ id: "sql-injection",
16712
+ detector: "sql_injection",
16713
+ positive: PHP(`$wpdb->get_results("SELECT * FROM t WHERE id=" . $id);`),
16714
+ negative: PHP(`$wpdb->get_results($wpdb->prepare("SELECT * FROM t WHERE id=%d", $id));`)
16715
+ },
16716
+ {
16717
+ id: "hardcoded-secret",
16718
+ detector: "hardcoded_secret",
16719
+ positive: PHP(`$api_key = "EXAMPLE_API_KEY_123456789";`),
16720
+ negative: PHP(`$api_key = getenv("API_KEY");`)
16721
+ },
16722
+ {
16723
+ id: "llm-injection",
16724
+ detector: "llm_injection",
16725
+ positive: PHP(`$u = $_POST['msg'];
16726
+ $r = openai.chat.completions.create(['messages' => [['role' => 'user', 'content' => $u]]]);`),
16727
+ negative: PHP(`$r = openai.chat.completions.create(['messages' => [['role' => 'user', 'content' => 'hello']]]);`)
16728
+ },
16729
+ {
16730
+ id: "graphql-exposure",
16731
+ detector: "graphql_exposure",
16732
+ positive: PHP(`$srv = new ApolloServer({ introspection: true, playground: true });`),
16733
+ negative: PHP(`$srv = new ApolloServer({ introspection: false });`)
16734
+ },
16735
+ {
16736
+ id: "oauth-misconfig",
16737
+ detector: "oauth_misconfig",
16738
+ positive: PHP(`$redirect_uri = $_GET['redirect_uri'];`),
16739
+ negative: PHP(`$redirect_uri = 'https://app.example.com/callback';`)
16740
+ },
16741
+ {
16742
+ id: "grpc-reflection",
16743
+ detector: "grpc_reflection",
16744
+ positive: PHP(`$server->addService(enable_server_reflection());`),
16745
+ negative: PHP(`$server->addService($my_service);`)
16746
+ }
16747
+ ];
16748
+ });
16749
+
16750
+ // src/sync.ts
16751
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, readFileSync as readFileSync24, readdirSync as readdirSync7, statSync as statSync4, rmSync as rmSync4 } from "node:fs";
16752
+ import { join as join21 } from "node:path";
16753
+ import { homedir as homedir10 } from "node:os";
16754
+ import { fileURLToPath as fileURLToPath11 } from "node:url";
16755
+ import { spawnSync as spawnSync4 } from "node:child_process";
16756
+ function dirExists(p) {
16757
+ try {
16758
+ return statSync4(p).isDirectory();
16759
+ } catch {
16760
+ return false;
16761
+ }
16762
+ }
16763
+ function countFiles(dir) {
16764
+ try {
16765
+ let n = 0;
16766
+ const walk = (d) => {
16767
+ for (const e of readdirSync7(d, { withFileTypes: true })) {
16768
+ if (e.isDirectory())
16769
+ walk(join21(d, e.name));
16770
+ else
16771
+ n++;
16772
+ }
16773
+ };
16774
+ walk(dir);
16775
+ return n;
16776
+ } catch {
16777
+ return 0;
16778
+ }
16779
+ }
16780
+ function syncData() {
16781
+ if (!dirExists(DATA_ROOT2))
16782
+ mkdirSync9(DATA_ROOT2, { recursive: true });
16783
+ const payloadsPath = join21(DATA_ROOT2, "payloads");
16784
+ const templatesPath = join21(DATA_ROOT2, "templates");
16785
+ const hasPayloads = dirExists(payloadsPath) && countFiles(payloadsPath) > 0;
16786
+ const hasTemplates = dirExists(templatesPath) && countFiles(templatesPath) > 0;
16787
+ if (hasPayloads && hasTemplates) {
16788
+ return {
16789
+ payloads: { path: payloadsPath, files: countFiles(payloadsPath), synced: false },
16790
+ templates: { path: templatesPath, files: countFiles(templatesPath), synced: false },
16791
+ data_root: DATA_ROOT2
16792
+ };
16793
+ }
16794
+ const localPayloads = join21(ROOT7, "payloads");
16795
+ const localTemplates = join21(ROOT7, "templates");
16796
+ let payloadsSynced = false;
16797
+ let templatesSynced = false;
16798
+ if (!hasPayloads && dirExists(localPayloads)) {
16799
+ copyDir(localPayloads, payloadsPath);
16800
+ payloadsSynced = true;
16801
+ }
16802
+ if (!hasTemplates && dirExists(localTemplates)) {
16803
+ copyDir(localTemplates, templatesPath);
16804
+ templatesSynced = true;
16805
+ }
16806
+ const gitOk = spawnSync4("git", ["--version"]).status === 0;
16807
+ if (gitOk && (!dirExists(payloadsPath) || countFiles(payloadsPath) === 0 || !dirExists(templatesPath) || countFiles(templatesPath) === 0)) {
16808
+ const tmp = join21(DATA_ROOT2, ".tmp-clone");
16809
+ if (dirExists(tmp))
16810
+ rmSync4(tmp, { recursive: true, force: true });
16811
+ const r = spawnSync4("git", ["clone", "--depth", "1", "--filter=blob:none", "--sparse", GITHUB_REPO, tmp], { timeout: 120000 });
16812
+ if (r.status === 0) {
16813
+ const sparse = spawnSync4("git", ["-C", tmp, "sparse-checkout", "set", "payloads", "templates"], { timeout: 120000 });
16814
+ if (sparse.status === 0) {
16815
+ if (!dirExists(payloadsPath) || countFiles(payloadsPath) === 0) {
16816
+ copyDir(join21(tmp, "payloads"), payloadsPath);
16817
+ payloadsSynced = true;
16818
+ }
16819
+ if (!dirExists(templatesPath) || countFiles(templatesPath) === 0) {
16820
+ copyDir(join21(tmp, "templates"), templatesPath);
16821
+ templatesSynced = true;
16822
+ }
16823
+ }
16824
+ rmSync4(tmp, { recursive: true, force: true });
16825
+ }
16826
+ }
16827
+ return {
16828
+ payloads: { path: payloadsPath, files: countFiles(payloadsPath), synced: payloadsSynced },
16829
+ templates: { path: templatesPath, files: countFiles(templatesPath), synced: templatesSynced },
16830
+ data_root: DATA_ROOT2
16831
+ };
16832
+ }
16833
+ function copyDir(src, dst) {
16834
+ const r = spawnSync4("cp", ["-r", src, dst], { timeout: 120000 });
16835
+ if (r.status !== 0) {
16836
+ manualCopy(src, dst);
16837
+ }
16838
+ }
16839
+ function manualCopy(src, dst) {
16840
+ mkdirSync9(dst, { recursive: true });
16841
+ for (const e of readdirSync7(src, { withFileTypes: true })) {
16842
+ const s = join21(src, e.name);
16843
+ const d = join21(dst, e.name);
16844
+ if (e.isDirectory())
16845
+ manualCopy(s, d);
16846
+ else
16847
+ writeFileSync8(d, readFileSync24(s));
16848
+ }
16849
+ }
16850
+ var ROOT7, DATA_ROOT2, GITHUB_REPO = "https://github.com/shinthink/blitzstrike.git";
16851
+ var init_sync = __esm(() => {
16852
+ ROOT7 = join21(fileURLToPath11(new URL(".", import.meta.url)), "..");
16853
+ DATA_ROOT2 = process.env.BLITZSTRIKE_DATA ?? join21(homedir10(), ".blitzstrike", "data");
16854
+ });
16855
+
16856
+ // src/update.ts
16857
+ async function checkLatestVersion(timeoutMs = 1e4) {
16858
+ try {
16859
+ const controller = new AbortController;
16860
+ const t = setTimeout(() => controller.abort(), timeoutMs);
16861
+ const res = await fetch("https://registry.npmjs.org/blitzstrike/latest", { signal: controller.signal });
16862
+ clearTimeout(t);
16863
+ if (!res.ok)
16864
+ return null;
16865
+ const data = await res.json();
16866
+ return data.version ?? null;
16867
+ } catch {
16868
+ return null;
16869
+ }
16870
+ }
16871
+ function semverCompare(a, b) {
16872
+ const pa = a.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
16873
+ const pb = b.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
16874
+ for (let i = 0;i < 3; i++) {
16875
+ const x = pa[i] ?? 0;
16876
+ const y = pb[i] ?? 0;
16877
+ if (x !== y)
16878
+ return x < y ? -1 : 1;
16879
+ }
16880
+ return 0;
16881
+ }
16882
+ async function runUpdate(currentVersion) {
16883
+ const latest = await checkLatestVersion();
16884
+ const outdated = latest !== null && semverCompare(currentVersion, latest) < 0;
16885
+ return {
16886
+ current: currentVersion,
16887
+ latest,
16888
+ outdated,
16889
+ update_command: "npm install -g blitzstrike@latest # or: bun add -g blitzstrike@latest",
16890
+ data_sync: syncData()
16891
+ };
16892
+ }
16893
+ var init_update = __esm(() => {
16894
+ init_sync();
16895
+ });
16896
+
16897
+ // src/release.ts
16898
+ import { readFileSync as readFileSync26, existsSync as existsSync12 } from "node:fs";
16899
+ import { join as join22 } from "node:path";
16900
+ import { fileURLToPath as fileURLToPath12 } from "node:url";
16901
+ import { execSync as execSync2 } from "node:child_process";
16902
+ function validateRelease() {
16903
+ const checks = [];
16904
+ checks.push({ name: "package.json version", ok: true, detail: VERSION });
16905
+ try {
16906
+ const changelog = readFileSync26(join22(ROOT8, "CHANGELOG.md"), "utf8");
16907
+ const m = changelog.match(/^##\s+\[([0-9]+\.[0-9]+\.[0-9]+)\]/m);
16908
+ const changelogVersion = m?.[1] ?? null;
16909
+ checks.push({
16910
+ name: "CHANGELOG version",
16911
+ ok: changelogVersion === VERSION,
16912
+ detail: changelogVersion ?? "no version header found"
16913
+ });
16914
+ } catch {
16915
+ checks.push({ name: "CHANGELOG version", ok: false, detail: "CHANGELOG.md not found" });
16916
+ }
16917
+ try {
16918
+ const tag = execSync2("git describe --tags --abbrev=0", { cwd: ROOT8, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
16919
+ checks.push({ name: "Git tag", ok: tag === `v${VERSION}`, detail: tag, soft: true });
16920
+ } catch {
16921
+ checks.push({ name: "Git tag", ok: true, detail: "unavailable (no git/tags)", soft: true });
16922
+ }
16923
+ try {
16924
+ const published = execSync2("npm view blitzstrike version", { encoding: "utf8", timeout: 8000, stdio: ["ignore", "pipe", "ignore"] }).trim();
16925
+ checks.push({ name: "npm registry version", ok: published === VERSION, detail: published, soft: true });
16926
+ } catch {
16927
+ checks.push({ name: "npm registry version", ok: true, detail: "unavailable (offline)", soft: true });
16928
+ }
16929
+ checks.push({
16930
+ name: "Lockfile",
16931
+ ok: lockfilePresent(),
16932
+ detail: lockfilePresent() ? "present (bun.lock / package-lock.json)" : "missing",
16933
+ soft: true
16934
+ });
16935
+ return { version: VERSION, checks, consistent: checks.filter((c) => !c.soft).every((c) => c.ok) };
16936
+ }
16937
+ function lockfilePresent() {
16938
+ return existsSync12(join22(ROOT8, "package-lock.json")) || existsSync12(join22(ROOT8, "bun.lock")) || existsSync12(join22(ROOT8, "bun.lockb"));
16939
+ }
16940
+ var ROOT8;
16941
+ var init_release = __esm(() => {
16942
+ init_version();
16943
+ ROOT8 = join22(fileURLToPath12(new URL(".", import.meta.url)), "..");
16944
+ });
16945
+
16946
+ // src/dependency.ts
16947
+ import { readFileSync as readFileSync27 } from "node:fs";
16948
+ import { join as join23 } from "node:path";
16949
+ import { fileURLToPath as fileURLToPath13 } from "node:url";
16950
+ function checkDependencies() {
16951
+ let pkg = {};
16952
+ try {
16953
+ pkg = JSON.parse(readFileSync27(join23(ROOT9, "package.json"), "utf8"));
16954
+ } catch {
16955
+ return { install_scripts: [], git_dependencies: [], total_dependencies: 0, safe: false };
16956
+ }
16957
+ const installScriptNames = ["preinstall", "install", "postinstall", "prepare"];
16958
+ const installScripts = installScriptNames.filter((n) => pkg.scripts?.[n]).map((n) => ({ name: n, command: pkg.scripts[n] }));
16959
+ const allDeps = {
16960
+ ...pkg.dependencies ?? {},
16961
+ ...pkg.devDependencies ?? {},
16962
+ ...pkg.peerDependencies ?? {},
16963
+ ...pkg.optionalDependencies ?? {}
16964
+ };
16965
+ const gitDependencies = Object.entries(allDeps).filter(([, v]) => /^(git\+|git:|github:|gitlab:|bitbucket:|\w+:\/\/)/.test(v)).map(([name, v]) => `${name}@${v}`);
16966
+ const total = Object.keys(allDeps).length;
16967
+ return {
16968
+ install_scripts: installScripts,
16969
+ git_dependencies: gitDependencies,
16970
+ total_dependencies: total,
16971
+ safe: installScripts.length === 0 && gitDependencies.length === 0
16972
+ };
16973
+ }
16974
+ var ROOT9;
16975
+ var init_dependency = __esm(() => {
16976
+ ROOT9 = join23(fileURLToPath13(new URL(".", import.meta.url)), "..");
16977
+ });
16978
+
16979
+ // src/cli.ts
16980
+ import { readFileSync as readFileSync28, existsSync as existsSync13, writeFileSync as writeFileSync9, mkdirSync as mkdirSync10 } from "node:fs";
16981
+ import { join as join24, dirname as dirname4 } from "node:path";
16982
+ import { homedir as homedir11 } from "node:os";
16983
+ import { fileURLToPath as fileURLToPath14 } from "node:url";
16984
+ import { spawnSync as spawnSync5 } from "node:child_process";
16985
+ function which(cmd) {
16986
+ return spawnSync5("sh", ["-c", 'command -v "$1" >/dev/null 2>&1', "sh", cmd]).status === 0;
16987
+ }
16988
+ function installedCount() {
16989
+ const tools = loadTools();
16990
+ const installed = tools.filter((t) => which(t.name)).map((t) => t.name);
16991
+ return { total: tools.length, installed, missing: tools.map((t) => t.name).filter((n) => !installed.includes(n)) };
16992
+ }
16993
+ async function runDoctor() {
16994
+ console.log(`BlitzStrike doctor — environment health check
16995
+ `);
16996
+ const issues = [];
16997
+ const bunOk = which("bun") || which("node");
16998
+ issues.push({
16999
+ name: "Runtime (bun/node)",
15742
17000
  status: bunOk ? "ok" : "fail",
15743
17001
  detail: bunOk ? `node ${process.version}` : "neither bun nor node found",
15744
17002
  fix: bunOk ? undefined : "install bun: curl -fsSL https://bun.sh/install | bash"
@@ -30999,70 +32257,9 @@ function recordAudit(event, opts = {}) {
30999
32257
 
31000
32258
  // src/evidence.ts
31001
32259
  init_version();
32260
+ init_secrets();
32261
+ init_secrets();
31002
32262
  import { createHash as createHash2 } from "node:crypto";
31003
-
31004
- // src/secrets.ts
31005
- var SECRET_FIELDS = "api[_-]?key|apikey|api[_-]?secret|access[_-]?key|access[_-]?token|secret[_-]?key|secret[_-]?access[_-]?key|auth[_-]?token|client[_-]?secret|consumer[_-]?secret|consumer[_-]?key|private[_-]?key|db[_-]?password|database[_-]?password|db[_-]?pass|passwd|password|jwt[_-]?secret|encryption[_-]?key|encryption[_-]?password|github[_-]?token|gitlab[_-]?token|stripe[_-]?key|sendgrid[_-]?api[_-]?key|datadog[_-]?api[_-]?key|cloudflare[_-]?api[_-]?key|slack[_-]?token|aws[_-]?access[_-]?key[_-]?id|aws[_-]?secret[_-]?access[_-]?key|aws[_-]?session[_-]?token|bot[_-]?token";
31006
- var SECRET_FORMATS = [
31007
- { name: "JWT", re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}/g },
31008
- { name: "AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/g },
31009
- { name: "GitHub PAT (classic)", re: /\bghp_[A-Za-z0-9]{20,}\b/g },
31010
- { name: "GitHub PAT (fine-grained)", re: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g },
31011
- { name: "OpenAI/Stripe key", re: /\bsk-[A-Za-z0-9]{20,}\b/g },
31012
- { name: "Stripe live/test key", re: /\b(?:sk|pk|rk)_(?:live|test)_[0-9a-zA-Z]{10,}\b/g },
31013
- { name: "Slack token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
31014
- { name: "Google API key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
31015
- { name: "Twilio account SID", re: /\bAC[0-9a-f]{32}\b/g },
31016
- { name: "SendGrid API key", re: /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g },
31017
- { name: "Mailgun API key", re: /\bkey-[0-9a-zA-Z]{32}\b/g },
31018
- { name: "PEM private key", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/g }
31019
- ];
31020
- function shannonEntropy(s) {
31021
- if (!s)
31022
- return 0;
31023
- const freq = new Map;
31024
- for (const c of s)
31025
- freq.set(c, (freq.get(c) ?? 0) + 1);
31026
- let e = 0;
31027
- for (const n of freq.values()) {
31028
- const p = n / s.length;
31029
- e -= p * Math.log2(p);
31030
- }
31031
- return e;
31032
- }
31033
- var REDACTION_PATTERNS = [
31034
- [/(authorization\s*[:=]\s*)(bearer\s+)?[A-Za-z0-9._~+/=-]{8,}/gi, "$1$2[REDACTED]"],
31035
- [/(api[_-]?key\s*[:=]\s*)["']?[A-Za-z0-9._-]{16,}["']?/gi, "$1[REDACTED]"],
31036
- [/(access[_-]?token\s*[:=]\s*)["']?[A-Za-z0-9._-]{16,}["']?/gi, "$1[REDACTED]"],
31037
- [/(secret\s*[:=]\s*)["']?[A-Za-z0-9._/-]{16,}["']?/gi, "$1[REDACTED]"],
31038
- [/(password\s*[:=]\s*)["']?[^"'\s,}&]{4,}["']?/gi, "$1[REDACTED]"],
31039
- [/(passwd\s*[:=]\s*)["']?[^"'\s,}&]{4,}["']?/gi, "$1[REDACTED]"],
31040
- [/(private[_-]?key\s*[:=]\s*)["']?[A-Za-z0-9+/=_-]{16,}["']?/gi, "$1[REDACTED]"],
31041
- [/(session\s*[:=]\s*)["']?[A-Za-z0-9._-]{12,}["']?/gi, "$1[REDACTED]"],
31042
- [/(cookie\s*[:=]\s*)["']?[A-Za-z0-9._-]{12,}["']?/gi, "$1[REDACTED]"],
31043
- [/(set-cookie\s*:\s*)[^\r\n]+/gi, "$1[REDACTED]"],
31044
- [/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}/g, "[REDACTED]"],
31045
- [/AKIA[0-9A-Z]{16}/g, "[REDACTED]"],
31046
- [/ghp_[A-Za-z0-9]{20,}/g, "[REDACTED]"],
31047
- [/github_pat_[A-Za-z0-9_]{20,}/g, "[REDACTED]"],
31048
- [/sk-[A-Za-z0-9]{20,}/g, "[REDACTED]"],
31049
- [/\b(?:sk|pk|rk)_(?:live|test)_[0-9a-zA-Z]{10,}\b/g, "[REDACTED]"],
31050
- [/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, "[REDACTED]"],
31051
- [/\bAIza[0-9A-Za-z_-]{35}\b/g, "[REDACTED]"],
31052
- [/\bAC[0-9a-f]{32}\b/g, "[REDACTED]"],
31053
- [/\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g, "[REDACTED]"],
31054
- [/\bkey-[0-9a-zA-Z]{32}\b/g, "[REDACTED]"],
31055
- [/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[REDACTED]"]
31056
- ];
31057
- function redactSecrets(text) {
31058
- let out = text;
31059
- for (const [re, repl] of REDACTION_PATTERNS) {
31060
- out = out.replace(re, repl);
31061
- }
31062
- return out;
31063
- }
31064
-
31065
- // src/evidence.ts
31066
32263
  function hasSecrets(text) {
31067
32264
  return redactSecrets(text) !== text;
31068
32265
  }
@@ -31327,6 +32524,7 @@ function rejectFinding(finding, status, reason, evidence) {
31327
32524
  }
31328
32525
 
31329
32526
  // src/sinks.ts
32527
+ init_secrets();
31330
32528
  var NEXT = {
31331
32529
  rce: "test the sink with a marker payload (eval -> '1+1' -> 2, then sleep vs no-sleep / write a marker file) — this is direct RCE",
31332
32530
  ssti: "probe {{7*7}} / {{7*'7'}} for evaluation; if it evaluates, it is SSTI -> RCE",
@@ -56084,1097 +57282,179 @@ async function browserAgent(op, args = []) {
56084
57282
  return browserBack();
56085
57283
  case "forward":
56086
57284
  return browserForward();
56087
- case "get_cookie":
56088
- return browserGetCookie(args[0] ?? "");
56089
- case "set_cookie":
56090
- return browserSetCookie(args[0] ?? "", args[1] ?? "");
56091
- }
56092
- }
56093
- function browserDiagnostics() {
56094
- return { console_errors: [...consoleErrors], page_errors: [...pageErrors] };
56095
- }
56096
-
56097
- // src/server.ts
56098
- init_mcp_status();
56099
-
56100
- // src/route-confusion.ts
56101
- var SOURCE_TOKENS = /\$_(GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|\$request->|\$req->|request\.(get|input|query|post)|getParameter\(|req\.(query|body|params)/;
56102
- function lines(code) {
56103
- return code.split(`
56104
- `);
56105
- }
56106
- function lineNo(code, pos) {
56107
- let n = 1;
56108
- for (let i = 0;i < pos; i++)
56109
- if (code.charCodeAt(i) === 10)
56110
- n++;
56111
- return n;
56112
- }
56113
- function userControlled(scope) {
56114
- return SOURCE_TOKENS.test(scope);
56115
- }
56116
- var DYNAMIC_DISPATCH_RE = /\b(call_user_func|call_user_func_array|forward|dispatch|resolve_route|invoke)\s*\(\s*(\$|\[)/gi;
56117
- var DYNAMIC_METHOD_RE = /(->|::)\s*\$[A-Za-z_]/g;
56118
- var BATCH_LOOP_RE = /\bforeach\s*\([^)]*(requests|batch|sub_requests|operations)\b/gi;
56119
- var ROUTE_MATCH_RE = /\b(preg_match|preg_match_all|strpos|stripos|str_contains)\s*\(/i;
56120
- var NORMALIZERS = /\b(strtolower|strtoupper|rtrim|urldecode|rawurldecode|parse_url|normalize|canonical)\b/i;
56121
- var WHITELIST_LOOKUP = /\$[A-Za-z_]\w*\s*\[\s*\$[A-Za-z_]\w*\s*\]/;
56122
- var CANONICALIZE = /\b(basename|realpath|normalize|canonical|resolve|toRealPath)\s*\(/i;
56123
- function detectDynamicDispatch(code, file, out) {
56124
- const ls = lines(code);
56125
- DYNAMIC_DISPATCH_RE.lastIndex = 0;
56126
- let m;
56127
- while ((m = DYNAMIC_DISPATCH_RE.exec(code)) !== null) {
56128
- const ln = lineNo(code, m.index);
56129
- const l = ls[ln - 1] ?? "";
56130
- const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
56131
- `);
56132
- if (!userControlled(l) && !userControlled(scope))
56133
- continue;
56134
- if (WHITELIST_LOOKUP.test(l) || WHITELIST_LOOKUP.test(scope))
56135
- continue;
56136
- out.push({
56137
- file,
56138
- line: ln,
56139
- type: "dynamic_dispatch",
56140
- category: "Route confusion (user-controlled dispatch)",
56141
- severity: "high",
56142
- evidence: l.trim(),
56143
- detail: `\`${m[1]}()\` invoked with an attacker-controlled callback/route — the request selects the handler.`
56144
- });
56145
- }
56146
- }
56147
- function detectDynamicMethod(code, file, out) {
56148
- const ls = lines(code);
56149
- DYNAMIC_METHOD_RE.lastIndex = 0;
56150
- let m;
56151
- while ((m = DYNAMIC_METHOD_RE.exec(code)) !== null) {
56152
- const ln = lineNo(code, m.index);
56153
- const l = ls[ln - 1] ?? "";
56154
- const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
56155
- `);
56156
- if (!userControlled(l) && !userControlled(scope))
56157
- continue;
56158
- if (WHITELIST_LOOKUP.test(l) || WHITELIST_LOOKUP.test(scope))
56159
- continue;
56160
- out.push({
56161
- file,
56162
- line: ln,
56163
- type: "dynamic_method_call",
56164
- category: "Route confusion (dynamic method dispatch)",
56165
- severity: "high",
56166
- evidence: l.trim(),
56167
- detail: "Dynamic method/static call `" + m[1] + "$var()` — the invoked symbol is attacker-influenced."
56168
- });
56169
- }
56170
- }
56171
- function detectDynamicInclude(code, file, out) {
56172
- const ls = lines(code);
56173
- const re = /\b(include|require)(_once)?\s*\(\s*(\$[A-Za-z_]\w*)/g;
56174
- let m;
56175
- while ((m = re.exec(code)) !== null) {
56176
- const ln = lineNo(code, m.index);
56177
- const l = ls[ln - 1] ?? "";
56178
- const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
56179
- `);
56180
- if (!userControlled(l) && !userControlled(scope))
56181
- continue;
56182
- if (CANONICALIZE.test(scope + `
56183
- ` + l))
56184
- continue;
56185
- if (WHITELIST_LOOKUP.test(l) || WHITELIST_LOOKUP.test(scope))
56186
- continue;
56187
- out.push({
56188
- file,
56189
- line: ln,
56190
- type: "dynamic_include",
56191
- category: "Route confusion (dynamic file include)",
56192
- severity: "high",
56193
- evidence: l.trim(),
56194
- detail: `\`${m[1]}(…)\` of a variable — potential LFI/RCE when the path is attacker-controlled.`
56195
- });
56196
- }
56197
- }
56198
- function detectBatchForwarding(code, file, out) {
56199
- BATCH_LOOP_RE.lastIndex = 0;
56200
- let m;
56201
- while ((m = BATCH_LOOP_RE.exec(code)) !== null) {
56202
- const ln = lineNo(code, m.index);
56203
- const body = lines(code).slice(ln - 1, ln + 8).join(`
56204
- `);
56205
- const forwards = /\b(call_user_func|forward|dispatch|resolve_route|invoke|handle|route)\s*\(/i.test(body);
56206
- if (!forwards)
56207
- continue;
56208
- const rechecks = /\b(current_user_can|permission_callback|check_ajax_referer|authorize|authorise|is_user_logged_in)\b/i.test(body);
56209
- if (rechecks)
56210
- continue;
56211
- out.push({
56212
- file,
56213
- line: ln,
56214
- type: "batch_forwarding",
56215
- category: "Route confusion (batch/proxy forwarding)",
56216
- severity: "high",
56217
- evidence: lines(code)[ln - 1]?.trim() ?? "",
56218
- detail: "Batch/proxy loop forwards to inner handlers with no per-route authorization re-check — an attacker may reach a protected handler."
56219
- });
56220
- }
56221
- }
56222
- function detectRouteNormalization(code, file, out) {
56223
- const ls = lines(code);
56224
- for (let i = 0;i < ls.length; i++) {
56225
- const l = ls[i];
56226
- if (!ROUTE_MATCH_RE.test(l))
56227
- continue;
56228
- const scope = ls.slice(Math.max(0, i - 3), i + 1).join(`
56229
- `);
56230
- if (!userControlled(scope))
56231
- continue;
56232
- if (NORMALIZERS.test(l) || NORMALIZERS.test(scope))
56233
- continue;
56234
- if (/preg_match\s*\(\s*["'][^"']*\^[^"']*\$[^"']*["']/.test(l))
56235
- continue;
56236
- out.push({
56237
- file,
56238
- line: i + 1,
56239
- type: "route_normalization_gap",
56240
- category: "Route confusion (missing path normalization)",
56241
- severity: "medium",
56242
- evidence: l.trim(),
56243
- detail: "Route matched against user input without visible normalization (strtolower/rtrim/urldecode) — route desync possible."
56244
- });
56245
- }
56246
- }
56247
- function detectRouteConfusion(code, file) {
56248
- const out = [];
56249
- detectDynamicDispatch(code, file, out);
56250
- detectDynamicMethod(code, file, out);
56251
- detectDynamicInclude(code, file, out);
56252
- detectBatchForwarding(code, file, out);
56253
- detectRouteNormalization(code, file, out);
56254
- return out;
56255
- }
56256
-
56257
- // src/security-state.ts
56258
- var SANITIZERS2 = {
56259
- esc_sql: { context: "sql", level: "sanitized" },
56260
- addslashes: { context: "sql", level: "sanitized" },
56261
- mysqli_real_escape_string: { context: "sql", level: "sanitized" },
56262
- mysql_real_escape_string: { context: "sql", level: "sanitized" },
56263
- pg_escape_string: { context: "sql", level: "sanitized" },
56264
- sanitize_text_field: { context: "html", level: "sanitized" },
56265
- sanitize_textarea_field: { context: "html", level: "sanitized" },
56266
- esc_html: { context: "html", level: "sanitized" },
56267
- esc_html__: { context: "html", level: "sanitized" },
56268
- htmlspecialchars: { context: "html", level: "sanitized" },
56269
- htmlentities: { context: "html", level: "sanitized" },
56270
- wp_kses: { context: "html", level: "sanitized" },
56271
- wp_kses_post: { context: "html", level: "sanitized" },
56272
- wp_kses_data: { context: "html", level: "sanitized" },
56273
- strip_tags: { context: "html", level: "sanitized" },
56274
- esc_attr: { context: "html_attr", level: "sanitized" },
56275
- esc_url: { context: "url", level: "sanitized" },
56276
- esc_url_raw: { context: "url", level: "sanitized" },
56277
- esc_textarea: { context: "html", level: "sanitized" },
56278
- esc_js: { context: "js", level: "sanitized" },
56279
- urlencode: { context: "url", level: "sanitized" },
56280
- rawurlencode: { context: "url", level: "sanitized" },
56281
- wp_json_encode: { context: "json", level: "sanitized" },
56282
- json_encode: { context: "json", level: "sanitized" },
56283
- absint: { context: "int", level: "validated" },
56284
- intval: { context: "int", level: "validated" },
56285
- sanitize_key: { context: "key", level: "sanitized" },
56286
- sanitize_email: { context: "email", level: "validated" },
56287
- sanitize_file_name: { context: "filename", level: "validated" },
56288
- sanitize_url: { context: "url", level: "validated" },
56289
- sanitize_title: { context: "slug", level: "validated" },
56290
- sanitize_title_with_dashes: { context: "slug", level: "validated" },
56291
- sanitize_user: { context: "username", level: "validated" },
56292
- filter_var: { context: "validated", level: "validated" },
56293
- wp_validate_boolean: { context: "validated", level: "validated" }
56294
- };
56295
- var PSEUDO_SANITIZERS = new Set([
56296
- "trim",
56297
- "ltrim",
56298
- "rtrim",
56299
- "strtolower",
56300
- "strtoupper",
56301
- "ucfirst",
56302
- "ucwords",
56303
- "base64_encode",
56304
- "base64_decode",
56305
- "urldecode",
56306
- "rawurldecode",
56307
- "stripslashes",
56308
- "html_entity_decode",
56309
- "htmlspecialchars_decode",
56310
- "json_decode",
56311
- "unserialize",
56312
- "serialize",
56313
- "substr",
56314
- "mb_substr",
56315
- "explode",
56316
- "implode",
56317
- "str_replace",
56318
- "preg_replace",
56319
- "nl2br",
56320
- "strval",
56321
- "number_format",
56322
- "sprintf",
56323
- "md5",
56324
- "sha1",
56325
- "hash",
56326
- "crypt",
56327
- "chr",
56328
- "ord",
56329
- "strrev",
56330
- "str_repeat",
56331
- "str_pad"
56332
- ]);
56333
- var SINK_REQUIREMENTS = {
56334
- sql_execution: ["sql", "sql_param", "int"],
56335
- code_execution: ["code"],
56336
- command_execution: ["code"],
56337
- deserialization: ["code"],
56338
- file_operations: ["filename"],
56339
- redirect: ["url"],
56340
- html_render: ["html"]
56341
- };
56342
- var SOURCE_TOKENS2 = /\$_(GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|->(input|query|request)\(|\breq\.(query|body|params|headers|cookies)|\bgetParameter\(/;
56343
- function analyzeSecurityState(input) {
56344
- let state = "tainted";
56345
- let context = null;
56346
- for (const fn of input.sanitizers) {
56347
- const s = SANITIZERS2[fn];
56348
- if (!s)
56349
- continue;
56350
- if (s.level === "validated") {
56351
- if (state === "tainted" || state === "sanitized") {
56352
- state = "validated";
56353
- context = s.context;
56354
- }
56355
- } else if (state === "tainted") {
56356
- state = "sanitized";
56357
- context = s.context;
56358
- }
56359
- }
56360
- const required = SINK_REQUIREMENTS[input.sinkType] ?? [];
56361
- if (required.length === 0) {
56362
- return { verdict: "unknown", state, context, required, reason: `sink type '${input.sinkType}' not in the requirements table` };
56363
- }
56364
- if (state === "tainted") {
56365
- return { verdict: "vulnerable", state, context, required, reason: `raw tainted input reaches a ${input.sinkType} sink with no sanitization` };
56366
- }
56367
- if (state === "validated") {
56368
- return { verdict: "safe", state, context, required, reason: `input validated (${context}) — safe for any sink` };
56369
- }
56370
- if (state === "sanitized") {
56371
- if (context && required.includes(context)) {
56372
- return { verdict: "safe", state, context, required, reason: `sanitized for '${context}' — matches sink requirement ${required.join("/")}` };
56373
- }
56374
- return { verdict: "vulnerable", state, context, required, reason: `sanitized for '${context}' but this sink requires ${required.join("/")} — WRONG sanitizer (context mismatch)` };
56375
- }
56376
- return { verdict: "unknown", state, context, required, reason: "unhandled state" };
56377
- }
56378
- function sinkTypeOf(line) {
56379
- if (/\$wpdb->(query|get_var|get_row|get_results|get_col)|mysqli_query\s*\(|mysql_query\s*\(|pg_query\s*\(|->query\s*\(/.test(line) && !/->prepare\s*\(/.test(line))
56380
- return "sql_execution";
56381
- if (/\b(eval|assert)\s*\(/.test(line))
56382
- return "code_execution";
56383
- if (/\b(system|exec|shell_exec|passthru|proc_open|popen)\s*\(/.test(line))
56384
- return "command_execution";
56385
- if (/\b(unserialize|maybe_unserialize)\s*\(/.test(line))
56386
- return "deserialization";
56387
- if (/\b(file_get_contents|file_put_contents|fopen|readfile|include|require|include_once|require_once)\s*\(/.test(line))
56388
- return "file_operations";
56389
- if (/header\s*\(\s*['"]Location/i.test(line))
56390
- return "redirect";
56391
- return null;
56392
- }
56393
- function detectWrongSanitizer(code, file) {
56394
- const out = [];
56395
- const ls = code.split(`
56396
- `);
56397
- const varSanitizer = new Map;
56398
- for (const line of ls) {
56399
- const m = line.match(/\$(\w+)\s*=\s*([a-zA-Z0-9_]+)\s*\(\s*([^)]*)\s*\)\s*;?/);
56400
- if (!m)
56401
- continue;
56402
- const [, varName, fn, arg] = m;
56403
- if (!SOURCE_TOKENS2.test(arg))
56404
- continue;
56405
- if (SANITIZERS2[fn])
56406
- varSanitizer.set(varName, fn);
56407
- else if (PSEUDO_SANITIZERS.has(fn))
56408
- varSanitizer.set(varName, fn);
56409
- }
56410
- for (let i = 0;i < ls.length; i++) {
56411
- const line = ls[i];
56412
- const sinkType = sinkTypeOf(line);
56413
- if (!sinkType)
56414
- continue;
56415
- const required = SINK_REQUIREMENTS[sinkType] ?? [];
56416
- for (const [varName, fn] of varSanitizer) {
56417
- if (!new RegExp(`\\$${varName}\\b`).test(line))
56418
- continue;
56419
- const sanitizer = SANITIZERS2[fn];
56420
- const context = sanitizer?.context ?? null;
56421
- if (sanitizer && (sanitizer.level === "validated" || context && required.includes(context)))
56422
- continue;
56423
- const detail = sanitizer ? `$${varName} was sanitized for '${context}' (${fn}()) but this ${sinkType} sink requires ${required.join("/")} — WRONG sanitizer (context mismatch).` : `$${varName} passed through ${fn}() — a NON-sanitizing transformation — and still reaches this ${sinkType} sink as raw tainted input. ${fn}() does not neutralize ${required.join("/")} context.`;
56424
- out.push({
56425
- file,
56426
- line: i + 1,
56427
- type: "wrong_sanitizer",
56428
- category: "Wrong-context sanitization / pseudo-sanitizer",
56429
- severity: "high",
56430
- evidence: line.trim(),
56431
- detail
56432
- });
56433
- }
56434
- }
56435
- const seen = new Set;
56436
- return out.filter((f) => {
56437
- const key = `${f.type}:${f.line}`;
56438
- if (seen.has(key))
56439
- return false;
56440
- seen.add(key);
56441
- return true;
56442
- });
56443
- }
56444
-
56445
- // src/framework-security.ts
56446
- var FRAMEWORKS = {
56447
- wordpress: {
56448
- framework: "wordpress",
56449
- auth_primitives: ["current_user_can", "current_user_can_edit_post", "is_user_logged_in", "authorize"],
56450
- nonce_primitives: ["wp_verify_nonce", "check_ajax_referer", "check_admin_referer", "verify_nonce"],
56451
- entry_point_hooks: ["wp_ajax_", "wp_ajax_nopriv_", "admin_post_", "admin_post_nopriv_", "register_rest_route", "add_shortcode", "init", "rest_api_init"],
56452
- sanitizers: ["sanitize_text_field", "esc_sql", "esc_html", "esc_attr", "esc_url", "wp_kses", "wp_kses_post", "absint", "sanitize_email", "sanitize_file_name"]
56453
- },
56454
- wp: {
56455
- framework: "wordpress",
56456
- auth_primitives: ["current_user_can", "is_user_logged_in", "authorize"],
56457
- nonce_primitives: ["wp_verify_nonce", "check_ajax_referer", "check_admin_referer"],
56458
- entry_point_hooks: ["wp_ajax_", "wp_ajax_nopriv_", "register_rest_route"],
56459
- sanitizers: ["sanitize_text_field", "esc_sql", "esc_html", "absint"]
56460
- },
56461
- laravel: {
56462
- framework: "laravel",
56463
- auth_primitives: ["auth", "authorize", "Gate::", "can:", "->middleware('auth'", "->can(", "$this->authorize", "@can", "@cannot", "policy"],
56464
- nonce_primitives: ["@csrf", "csrf_field", "VerifyCsrfToken"],
56465
- entry_point_hooks: ["Route::get", "Route::post", "Route::put", "Route::delete", "Route::resource", "Route::match"],
56466
- sanitizers: ["e(", "htmlspecialchars", "clean(", "strip_tags"]
56467
- },
56468
- django: {
56469
- framework: "django",
56470
- auth_primitives: ["login_required", "permission_required", "user_passes_test", "LoginRequiredMixin", "PermissionRequiredMixin", "permission_classes", "IsAuthenticated", "has_permission"],
56471
- nonce_primitives: ["csrf_protect", "CsrfViewMiddleware", "@csrf"],
56472
- entry_point_hooks: ["def ", "@api_view", "path(", "url(", "re_path("],
56473
- sanitizers: ["escape(", "mark_safe", "html.escape"]
56474
- },
56475
- flask: {
56476
- framework: "flask",
56477
- auth_primitives: ["login_required", "@login_required", "current_user.is_authenticated", "permission_required"],
56478
- nonce_primitives: ["CSRFProtect", "csrf_token"],
56479
- entry_point_hooks: ["@app.route", "@blueprint.route", "@bp.route"],
56480
- sanitizers: ["escape(", "Markup", "html.escape"]
56481
- },
56482
- express: {
56483
- framework: "express",
56484
- auth_primitives: ["requireAuth", "authenticate", "passport.authenticate", "req.user", "isAuthenticated", "ensureAuthenticated", "requireLogin", "authorize", "authMiddleware"],
56485
- nonce_primitives: ["csurf", "csrfProtection", "helmet"],
56486
- entry_point_hooks: ["app.get", "app.post", "router.get", "router.post"],
56487
- sanitizers: ["escapeHtml", "encodeURIComponent", "sanitize", "xss"]
56488
- },
56489
- spring: {
56490
- framework: "spring",
56491
- auth_primitives: ["@PreAuthorize", "@Secured", "@RolesAllowed", "hasRole", "hasAuthority", "isAuthenticated", "SecurityContextHolder"],
56492
- nonce_primitives: ["csrf", "CsrfToken"],
56493
- entry_point_hooks: ["@GetMapping", "@PostMapping", "@RequestMapping", "@Controller"],
56494
- sanitizers: ["HtmlUtils.htmlEscape", "escapeHtml", "StringEscapeUtils"]
56495
- },
56496
- symfony: {
56497
- framework: "symfony",
56498
- auth_primitives: ["is_granted", "denyAccessUnlessGranted", "->isGranted(", "IsGranted", "Voter"],
56499
- nonce_primitives: ["isCsrfTokenValid", "csrf_token"],
56500
- entry_point_hooks: ["#[Route", "@Route", "#[Get", "#[Post"],
56501
- sanitizers: ["e(", "htmlspecialchars", "escape("]
56502
- },
56503
- aspnet: {
56504
- framework: "aspnet",
56505
- auth_primitives: ["[Authorize", "[AllowAnonymous", "User.Identity", "IsInRole", "AuthorizeAsync", "IAuthorizationService"],
56506
- nonce_primitives: ["ValidateAntiForgeryToken", "Antiforgery"],
56507
- entry_point_hooks: ["[HttpGet", "[HttpPost", "[Route("],
56508
- sanitizers: ["HtmlEncoder", "Html.Raw", "UrlEncoder"]
56509
- },
56510
- rails: {
56511
- framework: "rails",
56512
- auth_primitives: ["authenticate_user!", "authorize", "before_action :authenticate", "before_action :authorize", "current_user"],
56513
- nonce_primitives: ["protect_from_forgery", "form_authenticity_token"],
56514
- entry_point_hooks: ["def ", "before_action"],
56515
- sanitizers: ["h(", "html_escape", "sanitize"]
56516
- }
56517
- };
56518
- function frameworkKnowledge(framework = "wordpress") {
56519
- return FRAMEWORKS[framework.toLowerCase()] ?? null;
56520
- }
56521
- function listFrameworks2() {
56522
- const s = new Set;
56523
- for (const k of Object.keys(FRAMEWORKS))
56524
- s.add(FRAMEWORKS[k].framework);
56525
- return [...s];
56526
- }
56527
- function lineNoOf(code, pos) {
56528
- let n = 1;
56529
- for (let i = 0;i < pos; i++)
56530
- if (code.charCodeAt(i) === 10)
56531
- n++;
56532
- return n;
56533
- }
56534
- function functionBody(code, name) {
56535
- const idx = code.search(new RegExp(`function\\s+${name}\\s*\\(`));
56536
- if (idx === -1)
56537
- return null;
56538
- const open = code.indexOf("{", idx);
56539
- if (open === -1)
56540
- return null;
56541
- let depth = 0;
56542
- for (let i = open;i < code.length; i++) {
56543
- if (code[i] === "{")
56544
- depth++;
56545
- else if (code[i] === "}") {
56546
- depth--;
56547
- if (depth === 0)
56548
- return code.slice(open, i + 1);
56549
- }
56550
- }
56551
- return code.slice(open);
56552
- }
56553
- function handlerName(expr) {
56554
- const quoted = expr.match(/['"]([A-Za-z_][A-Za-z0-9_]*)['"]/g);
56555
- if (quoted && quoted.length > 0)
56556
- return quoted[quoted.length - 1].replace(/['"]/g, "");
56557
- return null;
56558
- }
56559
- var INDIRECT_AUTH = /\b[a-zA-Z0-9_]*(auth|permission|cap|nonce|can_|check|verify|allow)[a-zA-Z0-9_]*\s*\(/;
56560
- var AUTH_RE = /current_user_can|is_user_logged_in|is_super_admin|current_user_can_edit_post|authorize/;
56561
- var NONCE_RE = /wp_verify_nonce|check_ajax_referer|check_admin_referer|verify_nonce/;
56562
- var DANGEROUS_OP_RE = /\b(update_option|add_option|delete_option|update_site_option|delete_site_option|update_post_meta|add_post_meta|delete_post_meta|update_user_meta|add_user_meta|delete_user_meta|update_metadata|add_metadata|delete_metadata|set_transient|delete_transient|wp_insert_post|wp_update_post|wp_delete_post|wp_trash_post|wp_untrash_post|wp_insert_comment|wp_update_comment|wp_delete_comment|wp_insert_user|wp_update_user|wp_delete_user|wp_create_user|wp_set_current_user|wp_set_auth_cookie|set_user_role|set_role|add_role|remove_role|add_cap|remove_cap|wp_mail|wp_new_user_notification|wp_handle_upload|move_uploaded_file|wp_upload_bits|wp_set_object_terms|wp_set_post_terms|wp_schedule_event|wp_clear_scheduled_hook|wp_redirect|file_put_contents|unlink|rename|wpdb->query|wpdb->insert|wpdb->update|wpdb->delete|wpdb->replace)\s*\(/;
56563
- function detectWpAuthz(code, file, out) {
56564
- const ajaxRe = /add_action\s*\(\s*['"](wp_ajax(?:_nopriv)?_\w+)['"]\s*,\s*(.+?)\s*\)/g;
56565
- let m;
56566
- while ((m = ajaxRe.exec(code)) !== null) {
56567
- const hook = m[1];
56568
- const handler = handlerName(m[2]);
56569
- if (!handler)
56570
- continue;
56571
- const nopriv = hook.startsWith("wp_ajax_nopriv_");
56572
- const body = functionBody(code, handler);
56573
- if (!body)
56574
- continue;
56575
- const hasCap = AUTH_RE.test(body) || INDIRECT_AUTH.test(body);
56576
- const hasNonce = NONCE_RE.test(body);
56577
- const dangerous = DANGEROUS_OP_RE.test(body);
56578
- const line = lineNoOf(code, m.index);
56579
- if (nopriv && !hasCap && dangerous) {
56580
- out.push({ file, line, type: "missing_authz", category: "Missing capability check (unauthenticated AJAX handler)", severity: "high", evidence: `add_action('${hook}', ...)`, detail: `Unauthenticated AJAX handler '${handler}()' performs a privileged/state-changing operation with no current_user_can()/authorization check — auth bypass / privilege escalation.` });
56581
- }
56582
- if (!hasNonce && dangerous) {
56583
- out.push({ file, line, type: "missing_nonce", category: "Missing nonce verification", severity: "medium", evidence: `add_action('${hook}', ...)`, detail: `AJAX handler '${handler}()' performs a state-changing operation without nonce verification (wp_verify_nonce / check_ajax_referer) — CSRF.` });
56584
- }
56585
- }
56586
- const adminRe = /add_action\s*\(\s*['"](admin_post(?:_nopriv)?_\w+)['"]\s*,\s*(.+?)\s*\)/g;
56587
- while ((m = adminRe.exec(code)) !== null) {
56588
- const hook = m[1];
56589
- const handler = handlerName(m[2]);
56590
- if (!handler)
56591
- continue;
56592
- const nopriv = hook.startsWith("admin_post_nopriv_");
56593
- const body = functionBody(code, handler);
56594
- if (!body)
56595
- continue;
56596
- const hasCap = AUTH_RE.test(body) || INDIRECT_AUTH.test(body);
56597
- const hasNonce = NONCE_RE.test(body);
56598
- const dangerous = DANGEROUS_OP_RE.test(body);
56599
- const line = lineNoOf(code, m.index);
56600
- if (nopriv && !hasCap && dangerous) {
56601
- out.push({ file, line, type: "missing_authz", category: "Missing capability check (unauthenticated admin-post handler)", severity: "high", evidence: `add_action('${hook}', ...)`, detail: `Unauthenticated admin-post handler '${handler}()' performs a privileged/state-changing operation with no authorization check — auth bypass / privilege escalation.` });
56602
- }
56603
- if (!hasNonce && dangerous) {
56604
- out.push({ file, line, type: "missing_nonce", category: "Missing nonce verification", severity: "medium", evidence: `add_action('${hook}', ...)`, detail: `admin-post handler '${handler}()' performs a state-changing operation without nonce verification — CSRF.` });
56605
- }
56606
- }
56607
- const restRe = /register_rest_route\s*\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]/g;
56608
- while ((m = restRe.exec(code)) !== null) {
56609
- const line = lineNoOf(code, m.index);
56610
- const window2 = code.slice(m.index, m.index + 800);
56611
- const pc = window2.match(/permission_callback['"]?\s*=>\s*(.+?)(?:,|\s*\n|\])/);
56612
- if (pc) {
56613
- const expr = pc[1].trim();
56614
- if (/__return_true/.test(expr)) {
56615
- out.push({ file, line, type: "missing_authz", category: "REST route explicitly public (__return_true)", severity: "medium", evidence: `register_rest_route('${m[1]}', '${m[2]}')`, detail: `REST route '${m[1]}${m[2]}' sets permission_callback to __return_true (always allow) — verify the endpoint does not expose sensitive data or perform privileged actions.` });
56616
- }
56617
- continue;
56618
- }
56619
- out.push({ file, line, type: "missing_authz", category: "REST route missing permission_callback", severity: "high", evidence: `register_rest_route('${m[1]}', '${m[2]}')`, detail: `REST route '${m[1]}${m[2]}' has no permission_callback — the endpoint defaults to public (auth bypass).` });
56620
- }
56621
- const formRe = /add_action\s*\(\s*['"](init|admin_init|wp_loaded|template_redirect|parse_request|wp)['"]\s*,\s*(.+?)\s*\)/g;
56622
- while ((m = formRe.exec(code)) !== null) {
56623
- const hook = m[1];
56624
- const handler = handlerName(m[2]);
56625
- if (!handler)
56626
- continue;
56627
- const body = functionBody(code, handler);
56628
- if (!body)
56629
- continue;
56630
- if (!/\$_(GET|POST|REQUEST|COOKIE|SERVER)\b|php:\/\/input|filter_input|file_get_contents\(['"]php:\/\/input/.test(body))
56631
- continue;
56632
- const hasCap = AUTH_RE.test(body) || INDIRECT_AUTH.test(body);
56633
- const hasNonce = NONCE_RE.test(body);
56634
- const dangerous = DANGEROUS_OP_RE.test(body);
56635
- const line = lineNoOf(code, m.index);
56636
- if (!hasNonce && dangerous) {
56637
- out.push({ file, line, type: "missing_nonce", category: "Missing nonce verification (direct request handler)", severity: "medium", evidence: `add_action('${hook}', ...)`, detail: `Request handler '${handler}()' (on '${hook}') performs a state-changing operation without nonce verification — CSRF.` });
56638
- }
56639
- if (!hasCap && dangerous) {
56640
- out.push({ file, line, type: "missing_authz", category: "Missing capability check (direct request handler)", severity: "high", evidence: `add_action('${hook}', ...)`, detail: `Request handler '${handler}()' (on '${hook}') performs a privileged/state-changing operation with no capability check — auth bypass / privilege escalation.` });
56641
- }
56642
- }
56643
- }
56644
- var ROUTE_DETECTORS = [
56645
- { framework: "laravel", entry: /Route::(?:get|post|put|patch|delete|any|match|resource)\s*\(\s*['"][^'"]+['"]/g, auth: /->middleware\s*\(\s*['"]auth|middleware\s*\(\s*\[[^\]]*['"]auth|->can\s*\(|\$this->authorize|Gate::|authorize\s*\(|@can\b/, window: 2 },
56646
- { framework: "django", entry: /@api_view\s*\(|def\s+\w+\s*\(\s*request\s*[,)]/g, auth: /login_required|permission_required|user_passes_test|LoginRequiredMixin|PermissionRequiredMixin|permission_classes|IsAuthenticated|has_permission/, window: 3 },
56647
- { framework: "flask", entry: /@(?:app|bp|blueprint)\.route\s*\(/g, auth: /login_required|current_user\.is_authenticated|permission_required/, window: 3 },
56648
- { framework: "express", entry: /(?:app|router)\.(?:get|post|put|delete|patch|use)\s*\(\s*['"][^'"]+['"]/g, auth: /requireAuth|authenticate|passport\.authenticate|req\.user|isAuthenticated|ensureAuthenticated|requireLogin|\.authorize|authMiddleware/, window: 2 },
56649
- { framework: "spring", entry: /@(?:Get|Post|Put|Delete|Patch|Request)Mapping\s*\(/g, auth: /@PreAuthorize|@Secured|@RolesAllowed|hasRole|hasAuthority|isAuthenticated|SecurityContextHolder/, window: 2 },
56650
- { framework: "symfony", entry: /#\[(?:Route|Get|Post|Put|Delete|Patch)\b|@Route\s*\(/g, auth: /is_granted|denyAccessUnlessGranted|->isGranted\s*\(|IsGranted|Voter/, window: 2 },
56651
- { framework: "aspnet", entry: /\[Http(?:Get|Post|Put|Delete|Patch)\b/g, auth: /\[Authorize|User\.Identity|IsInRole|AuthorizeAsync|\[AllowAnonymous|IAuthorizationService/, window: 2 },
56652
- { framework: "rails", entry: /class\s+\w+\s*<\s*ApplicationController/g, auth: /before_action\s+:authenticate|before_action\s+:authorize|authenticate_user!|authorize\s+|current_user|authenticate/, window: 6 }
56653
- ];
56654
- function detectRouteAuthz(code, file, out) {
56655
- const ls = code.split(`
56656
- `);
56657
- for (const d of ROUTE_DETECTORS) {
56658
- d.entry.lastIndex = 0;
56659
- let m;
56660
- while ((m = d.entry.exec(code)) !== null) {
56661
- const ln = lineNoOf(code, m.index);
56662
- const window2 = ls.slice(Math.max(0, ln - d.window), Math.min(ls.length, ln + d.window + 1)).join(`
56663
- `);
56664
- if (d.auth.test(window2))
56665
- continue;
56666
- out.push({
56667
- file,
56668
- line: ln,
56669
- type: "missing_authz",
56670
- category: `${d.framework}: endpoint without authorization`,
56671
- severity: "high",
56672
- evidence: (ls[ln - 1] ?? "").trim(),
56673
- detail: `${d.framework} endpoint has no authorization enforcement in the surrounding ${d.window}-line window — auth bypass / privilege escalation.`
56674
- });
56675
- }
57285
+ case "get_cookie":
57286
+ return browserGetCookie(args[0] ?? "");
57287
+ case "set_cookie":
57288
+ return browserSetCookie(args[0] ?? "", args[1] ?? "");
56676
57289
  }
56677
57290
  }
56678
- function detectMissingAuthz(code, file) {
56679
- const out = [];
56680
- detectWpAuthz(code, file, out);
56681
- detectRouteAuthz(code, file, out);
56682
- const seen = new Set;
56683
- return out.filter((f) => {
56684
- const key = `${f.type}:${f.line}`;
56685
- if (seen.has(key))
56686
- return false;
56687
- seen.add(key);
56688
- return true;
56689
- });
57291
+ function browserDiagnostics() {
57292
+ return { console_errors: [...consoleErrors], page_errors: [...pageErrors] };
56690
57293
  }
56691
57294
 
56692
- // src/complex-bugs.ts
56693
- var SOURCE_TOKENS3 = /\$_(GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|\$request->|\$req->|request\.(get|input|query|post|data|body|headers|cookies|params)|getParameter\(|req\.(query|body|params|headers|cookies)/;
56694
- function lines2(code) {
57295
+ // src/server.ts
57296
+ init_mcp_status();
57297
+
57298
+ // src/route-confusion.ts
57299
+ var SOURCE_TOKENS = /\$_(GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|\$request->|\$req->|request\.(get|input|query|post)|getParameter\(|req\.(query|body|params)/;
57300
+ function lines(code) {
56695
57301
  return code.split(`
56696
57302
  `);
56697
57303
  }
56698
- function lineNo2(code, pos) {
57304
+ function lineNo(code, pos) {
56699
57305
  let n = 1;
56700
57306
  for (let i = 0;i < pos; i++)
56701
57307
  if (code.charCodeAt(i) === 10)
56702
57308
  n++;
56703
57309
  return n;
56704
57310
  }
56705
- function stripCommentsOnly(code) {
56706
- const out = code.split("");
56707
- const n = code.length;
56708
- let i = 0;
56709
- while (i < n) {
56710
- const c = code[i];
56711
- const nc = code[i + 1];
56712
- if (c === "'" || c === '"') {
56713
- const q = c;
56714
- i++;
56715
- while (i < n && code[i] !== q) {
56716
- if (code[i] === "\\")
56717
- i++;
56718
- i++;
56719
- }
56720
- i++;
56721
- continue;
56722
- }
56723
- if (c === "/" && nc === "/" || c === "#") {
56724
- while (i < n && code[i] !== `
56725
- `) {
56726
- out[i] = " ";
56727
- i++;
56728
- }
56729
- continue;
56730
- }
56731
- if (c === "/" && nc === "*") {
56732
- out[i] = " ";
56733
- out[i + 1] = " ";
56734
- i += 2;
56735
- while (i < n && !(code[i] === "*" && code[i + 1] === "/")) {
56736
- if (code[i] !== `
56737
- `)
56738
- out[i] = " ";
56739
- i++;
56740
- }
56741
- if (i < n) {
56742
- out[i] = " ";
56743
- if (i + 1 < n)
56744
- out[i + 1] = " ";
56745
- i += 2;
56746
- }
56747
- continue;
56748
- }
56749
- i++;
56750
- }
56751
- return out.join("");
56752
- }
56753
- var MAGIC_METHODS = /\b(__destruct|__wakeup|__toString|__invoke|__call|__get|__set|__isset|__unset|__sleep)\s*\(/;
56754
- function detectDeserialization(code, file, out) {
56755
- if (!MAGIC_METHODS.test(code))
56756
- return;
56757
- const ls = lines2(code);
56758
- const re = /\b(unserialize|maybe_unserialize)\s*\(/g;
56759
- let m;
56760
- while ((m = re.exec(code)) !== null) {
56761
- const ln = lineNo2(code, m.index);
56762
- const l = ls[ln - 1] ?? "";
56763
- const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
56764
- `);
56765
- if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56766
- continue;
56767
- out.push({
56768
- file,
56769
- line: ln,
56770
- type: "deserialization",
56771
- category: "Deserialization → POP gadget chain",
56772
- severity: "high",
56773
- evidence: l.trim(),
56774
- detail: `\`${m[1]}()\` of attacker-controlled data AND a magic method is present — object injection can pivot to RCE via the gadget chain.`
56775
- });
56776
- }
57311
+ function userControlled(scope) {
57312
+ return SOURCE_TOKENS.test(scope);
56777
57313
  }
56778
- function detectTypeJuggling(code, file, out) {
56779
- const ls = lines2(code);
56780
- const re = /(?<![=!<>])(==|!=)(?!=)/g;
57314
+ var DYNAMIC_DISPATCH_RE = /\b(call_user_func|call_user_func_array|forward|dispatch|resolve_route|invoke)\s*\(\s*(\$|\[)/gi;
57315
+ var DYNAMIC_METHOD_RE = /(->|::)\s*\$[A-Za-z_]/g;
57316
+ var BATCH_LOOP_RE = /\bforeach\s*\([^)]*(requests|batch|sub_requests|operations)\b/gi;
57317
+ var ROUTE_MATCH_RE = /\b(preg_match|preg_match_all|strpos|stripos|str_contains)\s*\(/i;
57318
+ var NORMALIZERS = /\b(strtolower|strtoupper|rtrim|urldecode|rawurldecode|parse_url|normalize|canonical)\b/i;
57319
+ var WHITELIST_LOOKUP = /\$[A-Za-z_]\w*\s*\[\s*\$[A-Za-z_]\w*\s*\]/;
57320
+ var CANONICALIZE = /\b(basename|realpath|normalize|canonical|resolve|toRealPath)\s*\(/i;
57321
+ function detectDynamicDispatch(code, file, out) {
57322
+ const ls = lines(code);
57323
+ DYNAMIC_DISPATCH_RE.lastIndex = 0;
56781
57324
  let m;
56782
- while ((m = re.exec(code)) !== null) {
56783
- const ln = lineNo2(code, m.index);
57325
+ while ((m = DYNAMIC_DISPATCH_RE.exec(code)) !== null) {
57326
+ const ln = lineNo(code, m.index);
56784
57327
  const l = ls[ln - 1] ?? "";
56785
57328
  const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
56786
57329
  `);
56787
- const hasSource = SOURCE_TOKENS3.test(l) || SOURCE_TOKENS3.test(scope);
56788
- const hasHash = /\b(md5|sha1|sha256|hash|crc32|crypt|password_verify|strcmp|hash_equals|secret|token|nonce)\s*\(?/i.test(l) || /\b(secret|token|nonce|hash)\b/i.test(l);
56789
- if (!hasSource || !hasHash)
57330
+ if (!userControlled(l) && !userControlled(scope))
56790
57331
  continue;
56791
- out.push({
56792
- file,
56793
- line: ln,
56794
- type: "type_juggling",
56795
- category: "Type juggling (loose comparison)",
56796
- severity: "medium",
56797
- evidence: l.trim(),
56798
- detail: 'Loose comparison of user input against a hash/secret — PHP type juggling (`0e…`, `"str" == 0`) may bypass the check. Use strict `===`.'
56799
- });
56800
- }
56801
- }
56802
- function detectMassAssignment(code, file, out) {
56803
- const ls = lines2(code);
56804
- const extractRe = /\bextract\s*\(\s*[^)]*(REQUEST|POST|GET|COOKIE|FILES|\$_(REQUEST|POST|GET|COOKIE|FILES))/g;
56805
- let m;
56806
- while ((m = extractRe.exec(code)) !== null) {
56807
- const ln = lineNo2(code, m.index);
56808
- const l = ls[ln - 1] ?? "";
56809
- if (/EXTR_SKIP|EXTR_PREFIX_ALL/.test(l))
57332
+ if (WHITELIST_LOOKUP.test(l) || WHITELIST_LOOKUP.test(scope))
56810
57333
  continue;
56811
57334
  out.push({
56812
57335
  file,
56813
57336
  line: ln,
56814
- type: "mass_assignment",
56815
- category: "Mass assignment (variable injection)",
57337
+ type: "dynamic_dispatch",
57338
+ category: "Route confusion (user-controlled dispatch)",
56816
57339
  severity: "high",
56817
57340
  evidence: l.trim(),
56818
- detail: "`extract()` of request data without EXTR_SKIP — attacker can overwrite arbitrary variables."
56819
- });
56820
- }
56821
- const parseRe = /\bparse_str\s*\(/g;
56822
- while ((m = parseRe.exec(code)) !== null) {
56823
- const ln = lineNo2(code, m.index);
56824
- const l = ls[ln - 1] ?? "";
56825
- const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
56826
- `);
56827
- if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56828
- continue;
56829
- out.push({
56830
- file,
56831
- line: ln,
56832
- type: "mass_assignment",
56833
- category: "Mass assignment (variable injection)",
56834
- severity: "medium",
56835
- evidence: l.trim(),
56836
- detail: "`parse_str()` of user input into the symbol table — variable injection."
57341
+ detail: `\`${m[1]}()\` invoked with an attacker-controlled callback/route — the request selects the handler.`
56837
57342
  });
56838
57343
  }
56839
57344
  }
56840
- function detectComplexBugs(code, file) {
56841
- const out = [];
56842
- detectDeserialization(code, file, out);
56843
- detectTypeJuggling(code, file, out);
56844
- detectMassAssignment(code, file, out);
56845
- detectPrototypePollution(code, file, out);
56846
- detectCrlfInjection(code, file, out);
56847
- detectPathConfusion(code, file, out);
56848
- detectSsrF(code, file, out);
56849
- detectXxe(code, file, out);
56850
- detectSsti(code, file, out);
56851
- detectFileUploadRce(code, file, out);
56852
- detectUnrestrictedUpload(code, file, out);
56853
- detectSqlInjection(code, file, out);
56854
- detectHardcodedSecret(code, file, out);
56855
- out.push(...detectWrongSanitizer(code, file));
56856
- out.push(...detectMissingAuthz(code, file));
56857
- const seen = new Set;
56858
- return out.filter((f) => {
56859
- const key = `${f.type}:${f.line}`;
56860
- if (seen.has(key))
56861
- return false;
56862
- seen.add(key);
56863
- return true;
56864
- });
56865
- }
56866
- function detectPrototypePollution(code, file, out) {
56867
- const ls = lines2(code);
56868
- const mergeRe = /\b(Object\.assign|_\.merge|_\.defaultsDeep|\.extend|\.merge|\.defaultsDeep|mergeDeep|deepMerge|_.set)\s*\(/g;
57345
+ function detectDynamicMethod(code, file, out) {
57346
+ const ls = lines(code);
57347
+ DYNAMIC_METHOD_RE.lastIndex = 0;
56869
57348
  let m;
56870
- while ((m = mergeRe.exec(code)) !== null) {
56871
- const ln = lineNo2(code, m.index);
57349
+ while ((m = DYNAMIC_METHOD_RE.exec(code)) !== null) {
57350
+ const ln = lineNo(code, m.index);
56872
57351
  const l = ls[ln - 1] ?? "";
56873
57352
  const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
56874
57353
  `);
56875
- if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56876
- continue;
56877
- out.push({
56878
- file,
56879
- line: ln,
56880
- type: "prototype_pollution",
56881
- category: "Prototype pollution (unsafe merge)",
56882
- severity: "high",
56883
- evidence: l.trim(),
56884
- detail: "Merge/extend of request data into an object without a `__proto__`/`constructor.prototype` guard — attacker-controlled keys can pollute `Object.prototype` and pivot to RCE or property injection."
56885
- });
56886
- }
56887
- }
56888
- function detectCrlfInjection(code, file, out) {
56889
- const ls = lines2(code);
56890
- const headerRe = /\b(header|setHeader|addHeader|writeHead|appendHeader)\s*\(/g;
56891
- let m;
56892
- while ((m = headerRe.exec(code)) !== null) {
56893
- const ln = lineNo2(code, m.index);
56894
- const l = ls[ln - 1] ?? "";
56895
- const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
56896
- `);
56897
- if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56898
- continue;
56899
- out.push({
56900
- file,
56901
- line: ln,
56902
- type: "crlf_injection",
56903
- category: "CRLF / header injection",
56904
- severity: "medium",
56905
- evidence: l.trim(),
56906
- detail: "User input reaches an HTTP header — CRLF (`%0d%0a`) can split the response, inject headers, or poison cookies. Strip newlines from header values."
56907
- });
56908
- }
56909
- }
56910
- function detectPathConfusion(code, file, out) {
56911
- const ls = lines2(code);
56912
- const fileRe = /\b(file_get_contents|file_put_contents|fopen|readFileSync|readFile|open|include|require|new\s+File|Files\.read|FileInputStream|FileOutputStream)\s*\(/g;
56913
- let m;
56914
- while ((m = fileRe.exec(code)) !== null) {
56915
- const ln = lineNo2(code, m.index);
56916
- const l = ls[ln - 1] ?? "";
56917
- const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
56918
- `);
56919
- if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56920
- continue;
56921
- if (/\b(basename|realpath|canonicalPath|getCanonicalPath|normalize|resolve|toRealPath)\s*\(/i.test(scope + `
56922
- ` + l))
56923
- continue;
56924
- if (/\$[A-Za-z_]\w*\s*\[\s*\$[A-Za-z_]\w*\s*\]/.test(scope + `
56925
- ` + l))
56926
- continue;
56927
- if (/["'][^"']*:\/\/[^"']*["']/.test(scope + `
56928
- ` + l))
56929
- continue;
56930
- out.push({
56931
- file,
56932
- line: ln,
56933
- type: "path_confusion",
56934
- category: "Path confusion (unsanitized file path)",
56935
- severity: "medium",
56936
- evidence: l.trim(),
56937
- detail: "User input reaches a file path with no canonicalization (`basename`/`realpath`/`normalize`) — traversal, `..%2f`, backslash and null-byte variants can confuse a naive `../` filter."
56938
- });
56939
- }
56940
- }
56941
- function detectSsrF(code, file, out) {
56942
- const ls = lines2(code);
56943
- const re = /\b(file_get_contents|fopen|file|readfile|curl_setopt|curl_init|fetch|axios|requests\.(get|post|put)|urllib\.request|http_get|wp_remote_(get|post)|new\s+URL\s*\(|got\(|http\.(get|request))\s*\(/g;
56944
- let m;
56945
- while ((m = re.exec(code)) !== null) {
56946
- const ln = lineNo2(code, m.index);
56947
- const l = ls[ln - 1] ?? "";
56948
- const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
56949
- `);
56950
- if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56951
- continue;
56952
- if (/(allowlist|whitelist|in_array|preg_match|parse_url|startsWith\(["']https?|endsWith\(["']|basename|realpath|normalize)/i.test(scope + `
56953
- ` + l))
57354
+ if (!userControlled(l) && !userControlled(scope))
56954
57355
  continue;
56955
- if (/["'][^"']*:\/\/[^"']*["']/.test(scope + `
56956
- ` + l))
57356
+ if (WHITELIST_LOOKUP.test(l) || WHITELIST_LOOKUP.test(scope))
56957
57357
  continue;
56958
57358
  out.push({
56959
57359
  file,
56960
57360
  line: ln,
56961
- type: "ssrf",
56962
- category: "SSRF (user-controlled URL)",
57361
+ type: "dynamic_method_call",
57362
+ category: "Route confusion (dynamic method dispatch)",
56963
57363
  severity: "high",
56964
57364
  evidence: l.trim(),
56965
- detail: "User input reaches a URL-accepting sink with no host allowlist — SSRF (cloud metadata, internal port scan, IAM credential theft)."
57365
+ detail: "Dynamic method/static call `" + m[1] + "$var()` — the invoked symbol is attacker-influenced."
56966
57366
  });
56967
57367
  }
56968
57368
  }
56969
- function detectXxe(code, file, out) {
56970
- const ls = lines2(code);
56971
- const re = /\b(simplexml_load_string|simplexml_load_file|new\s+SimpleXMLElement|->loadXML\s*\(|->load\s*\(|XMLReader\s*->\s*XML\s*\(|libxml_parse)\s*\(?/g;
57369
+ function detectDynamicInclude(code, file, out) {
57370
+ const ls = lines(code);
57371
+ const re = /\b(include|require)(_once)?\s*\(\s*(\$[A-Za-z_]\w*)/g;
56972
57372
  let m;
56973
57373
  while ((m = re.exec(code)) !== null) {
56974
- const ln = lineNo2(code, m.index);
57374
+ const ln = lineNo(code, m.index);
56975
57375
  const l = ls[ln - 1] ?? "";
56976
57376
  const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
56977
57377
  `);
56978
- if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
57378
+ if (!userControlled(l) && !userControlled(scope))
56979
57379
  continue;
56980
- if (/libxml_disable_entity_loader\s*\(\s*true|LIBXML_NONET/.test(scope + `
57380
+ if (CANONICALIZE.test(scope + `
56981
57381
  ` + l))
56982
57382
  continue;
56983
- out.push({
56984
- file,
56985
- line: ln,
56986
- type: "xxe",
56987
- category: "XXE (unprotected XML parse)",
56988
- severity: "high",
56989
- evidence: l.trim(),
56990
- detail: "User-controlled XML parsed without disabling external entities — XXE (file read, SSRF, billion-laughs DoS)."
56991
- });
56992
- }
56993
- }
56994
- function detectSsti(code, file, out) {
56995
- const ls = lines2(code);
56996
- const re = /\b(render_template_string|Template\s*\(|res\.render\s*\(|ejs\.(render|compile)\s*\(|_\.template\s*\(|render_string|twig\.render|Jinja2\(|jinja2\.Template)\s*\(?/g;
56997
- let m;
56998
- while ((m = re.exec(code)) !== null) {
56999
- const ln = lineNo2(code, m.index);
57000
- const l = ls[ln - 1] ?? "";
57001
- const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
57002
- `);
57003
- if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
57383
+ if (WHITELIST_LOOKUP.test(l) || WHITELIST_LOOKUP.test(scope))
57004
57384
  continue;
57005
57385
  out.push({
57006
57386
  file,
57007
57387
  line: ln,
57008
- type: "ssti",
57009
- category: "SSTI (template render of user input)",
57388
+ type: "dynamic_include",
57389
+ category: "Route confusion (dynamic file include)",
57010
57390
  severity: "high",
57011
57391
  evidence: l.trim(),
57012
- detail: "User input reaches a template render with no sandbox — SSTI (`{{7*7}}`, `${7*7}`, `#{7*7}`) → RCE."
57392
+ detail: `\`${m[1]}(…)\` of a variable — potential LFI/RCE when the path is attacker-controlled.`
57013
57393
  });
57014
57394
  }
57015
57395
  }
57016
- function detectFileUploadRce(code, file, out) {
57017
- code = stripCommentsOnly(code);
57018
- const ls = lines2(code);
57019
- const re = /\b(move_uploaded_file|wp_upload_bits|->move\s*\(|storeAs\s*\(|->storeAs\s*\(|putFile\s*\()\s*\(/g;
57396
+ function detectBatchForwarding(code, file, out) {
57397
+ BATCH_LOOP_RE.lastIndex = 0;
57020
57398
  let m;
57021
- while ((m = re.exec(code)) !== null) {
57022
- const ln = lineNo2(code, m.index);
57023
- const l = ls[ln - 1] ?? "";
57024
- const scope = ls.slice(Math.max(0, ln - 30), ln + 3).join(`
57399
+ while ((m = BATCH_LOOP_RE.exec(code)) !== null) {
57400
+ const ln = lineNo(code, m.index);
57401
+ const body = lines(code).slice(ln - 1, ln + 8).join(`
57025
57402
  `);
57026
- if (!/\$_FILES\b|tmp_name|getUploadedFile|uploadedFile|\['name'\]/i.test(scope))
57403
+ const forwards = /\b(call_user_func|forward|dispatch|resolve_route|invoke|handle|route)\s*\(/i.test(body);
57404
+ if (!forwards)
57027
57405
  continue;
57028
- if (/(wp_check_filetype|PATHINFO_EXTENSION|wp_get_mime_types|allowed_?(types|extensions|exts)|mime_?(types|extensions)|in_array\s*\([^)]*\$(?:allowed|whitelist|ext)|wp_handle_upload\s*\(\s*[^)]*test_form\s*=>\s*true)/i.test(scope))
57406
+ const rechecks = /\b(current_user_can|permission_callback|check_ajax_referer|authorize|authorise|is_user_logged_in)\b/i.test(body);
57407
+ if (rechecks)
57029
57408
  continue;
57030
57409
  out.push({
57031
57410
  file,
57032
57411
  line: ln,
57033
- type: "file_upload",
57034
- category: "File upload → RCE (unvalidated extension)",
57412
+ type: "batch_forwarding",
57413
+ category: "Route confusion (batch/proxy forwarding)",
57035
57414
  severity: "high",
57036
- evidence: l.trim(),
57037
- detail: "An uploaded file is written with an attacker-controlled name and no visible extension/filetype validation — upload a .php/.phtml shell → RCE."
57038
- });
57039
- }
57040
- }
57041
- function detectUnrestrictedUpload(code, file, out) {
57042
- const ls = lines2(code);
57043
- const re = /['"]uploadAllow['"]\s*=>\s*(?:array\s*\(\s*)?['"]all['"]/g;
57044
- let m;
57045
- while ((m = re.exec(code)) !== null) {
57046
- const ln = lineNo2(code, m.index);
57047
- const l = ls[ln - 1] ?? "";
57048
- const scope = ls.slice(Math.max(0, ln - 4), ln + 4).join(`
57049
- `);
57050
- const denyFirst = /['"]uploadOrder['"]\s*=>\s*array\s*\(\s*['"]deny['"]/.test(scope);
57051
- out.push({
57052
- file,
57053
- line: ln,
57054
- type: "file_upload",
57055
- category: "Unrestricted upload config (uploadAllow=all)",
57056
- severity: denyFirst ? "high" : "medium",
57057
- evidence: l.trim(),
57058
- detail: denyFirst ? "File-manager/connector config sets uploadAllow='all' with uploadOrder deny→allow — every mimetype (including .php) is uploadable → RCE." : "File-manager/connector config sets uploadAllow='all' — any file type can be uploaded; verify the upload path is not web-reachable or that a server-side extension whitelist exists."
57415
+ evidence: lines(code)[ln - 1]?.trim() ?? "",
57416
+ detail: "Batch/proxy loop forwards to inner handlers with no per-route authorization re-check — an attacker may reach a protected handler."
57059
57417
  });
57060
57418
  }
57061
57419
  }
57062
- function detectSqlInjection(code, file, out) {
57063
- const ls = lines2(code);
57064
- const sinkRe = /\$wpdb->(get_results|get_col|get_var|get_row|query)\s*\(|->query\s*\(|DB::unprepared\s*\(|->whereRaw\s*\(|->selectRaw\s*\(/g;
57065
- let m;
57066
- while ((m = sinkRe.exec(code)) !== null) {
57067
- const ln = lineNo2(code, m.index);
57068
- const l = ls[ln - 1] ?? "";
57069
- const scope = ls.slice(Math.max(0, ln - 6), ln + 1).join(`
57420
+ function detectRouteNormalization(code, file, out) {
57421
+ const ls = lines(code);
57422
+ for (let i = 0;i < ls.length; i++) {
57423
+ const l = ls[i];
57424
+ if (!ROUTE_MATCH_RE.test(l))
57425
+ continue;
57426
+ const scope = ls.slice(Math.max(0, i - 3), i + 1).join(`
57070
57427
  `);
57071
- if (/->prepare\s*\(/.test(scope))
57428
+ if (!userControlled(scope))
57072
57429
  continue;
57073
- if (SOURCE_TOKENS3.test(scope))
57430
+ if (NORMALIZERS.test(l) || NORMALIZERS.test(scope))
57074
57431
  continue;
57075
- const inlineConcat = /\.\s*\$[A-Za-z_]\w*/.test(l);
57076
- const assignConcat = /(?:^|\n)\s*\$[A-Za-z_]\w*\s*(?:\.=|=)\s*[^;\n]*["'][^"']*["']\s*\.\s*\$[A-Za-z_]\w*/.test(scope);
57077
- if (!inlineConcat && !assignConcat)
57432
+ if (/preg_match\s*\(\s*["'][^"']*\^[^"']*\$[^"']*["']/.test(l))
57078
57433
  continue;
57079
57434
  out.push({
57080
57435
  file,
57081
- line: ln,
57082
- type: "sql_injection",
57083
- category: "SQL injection (raw string-interpolated query)",
57436
+ line: i + 1,
57437
+ type: "route_normalization_gap",
57438
+ category: "Route confusion (missing path normalization)",
57084
57439
  severity: "medium",
57085
57440
  evidence: l.trim(),
57086
- detail: "A SQL sink receives a query built by string concatenation (not prepared), with no visible source in this file — trace the concatenated variable back to a request source; if user-controlled, this is SQL injection (CWE-89)."
57441
+ detail: "Route matched against user input without visible normalization (strtolower/rtrim/urldecode) — route desync possible."
57087
57442
  });
57088
57443
  }
57089
57444
  }
57090
- var SECRET_FIELD_RE = new RegExp(`\\b(${SECRET_FIELDS})\\s*(?:::|=>|=|:)\\s*`, "gi");
57091
- var SECRET_PLACEHOLDER_RE = /^(null|true|false|undefined|your_|xxx+|changeme|example|placeholder|test|secret|password|token|key|api|smtp|localhost|root|admin|pass|change|me|none|na|foo|bar|dummy)$/i;
57092
- var ENTROPY_EXCLUDE_VAR = /\b(data|payload|content|body|base64|encoded|encode|image|binary|blob|hash|digest|checksum|nonce|uuid|etag|signature|sig|cipher|iv|ciphertext|plaintext|thumb|thumbnail)\b/i;
57093
- function detectHardcodedSecret(code, file, out) {
57094
- const ls = lines2(code);
57095
- const clean = stripCommentsOnly(code);
57096
- const flaggedLines = new Set;
57097
- SECRET_FIELD_RE.lastIndex = 0;
57098
- let m;
57099
- while ((m = SECRET_FIELD_RE.exec(clean)) !== null) {
57100
- const field = m[1];
57101
- const rest = clean.slice(m.index + m[0].length);
57102
- const lineEnd = rest.search(/\r?\n/);
57103
- const valueRaw = (lineEnd === -1 ? rest : rest.slice(0, lineEnd)).trim();
57104
- let value = "";
57105
- const qm = valueRaw.match(/^["']([^"']{4,})["']/);
57106
- if (qm)
57107
- value = qm[1];
57108
- else
57109
- value = (valueRaw.match(/^([A-Za-z0-9._/+=@-]{8,})/) ?? [])[1] ?? "";
57110
- if (!value)
57111
- continue;
57112
- if (SECRET_PLACEHOLDER_RE.test(value))
57113
- continue;
57114
- if (/\$[A-Za-z_]|[{}\[\]]|process\.env|getenv|os\.environ|config\s*\(|env\s*\(/.test(valueRaw))
57115
- continue;
57116
- const ln = lineNo2(code, m.index);
57117
- flaggedLines.add(ln);
57118
- out.push({
57119
- file,
57120
- line: ln,
57121
- type: "hardcoded_secret",
57122
- category: "Hardcoded secret (credential in source)",
57123
- severity: "high",
57124
- evidence: (ls[ln - 1] ?? "").trim(),
57125
- detail: `Secret field '${field}' is assigned a hardcoded value in source — the credential is exposed to anyone with code access (CWE-798 Hard-coded Credentials). Rotate it and load from the environment instead.`
57126
- });
57127
- }
57128
- for (const fmt of SECRET_FORMATS) {
57129
- fmt.re.lastIndex = 0;
57130
- let fm;
57131
- while ((fm = fmt.re.exec(clean)) !== null) {
57132
- const ln = lineNo2(code, fm.index);
57133
- if (flaggedLines.has(ln))
57134
- continue;
57135
- flaggedLines.add(ln);
57136
- out.push({
57137
- file,
57138
- line: ln,
57139
- type: "hardcoded_secret",
57140
- category: "Hardcoded secret (known format)",
57141
- severity: "high",
57142
- evidence: (ls[ln - 1] ?? "").trim(),
57143
- detail: `A ${fmt.name} was found hardcoded in source — the credential is exposed to anyone with code access (CWE-798). Rotate it and load from the environment instead.`
57144
- });
57145
- }
57146
- }
57147
- const isMinified = /\.min\.(js|css)$/i.test(file) || /[\\/](bundle|vendor|dist|build)[\\/]/i.test(file);
57148
- if (!isMinified) {
57149
- const entropyRe = /(\b[A-Za-z_]\w*)\s*(?:::|=>|=|:)\s*["']([A-Za-z0-9_-]{32,})["']/g;
57150
- entropyRe.lastIndex = 0;
57151
- let em;
57152
- while ((em = entropyRe.exec(clean)) !== null) {
57153
- const varName = em[1];
57154
- const value = em[2];
57155
- const ln = lineNo2(code, em.index);
57156
- if (flaggedLines.has(ln))
57157
- continue;
57158
- if (ENTROPY_EXCLUDE_VAR.test(varName))
57159
- continue;
57160
- if (!/[A-Z]/.test(value) || !/[a-z]/.test(value) || !/[0-9]/.test(value))
57161
- continue;
57162
- if (shannonEntropy(value) < 4.5)
57163
- continue;
57164
- flaggedLines.add(ln);
57165
- out.push({
57166
- file,
57167
- line: ln,
57168
- type: "hardcoded_secret",
57169
- category: "Suspected secret (high-entropy token)",
57170
- severity: "medium",
57171
- evidence: (ls[ln - 1] ?? "").trim(),
57172
- detail: `'${varName}' is assigned a high-entropy token (Shannon ${shannonEntropy(value).toFixed(2)} bits/char) that looks like a generated credential — verify it is not a secret key/token (CWE-798).`
57173
- });
57174
- }
57175
- }
57445
+ function detectRouteConfusion(code, file) {
57446
+ const out = [];
57447
+ detectDynamicDispatch(code, file, out);
57448
+ detectDynamicMethod(code, file, out);
57449
+ detectDynamicInclude(code, file, out);
57450
+ detectBatchForwarding(code, file, out);
57451
+ detectRouteNormalization(code, file, out);
57452
+ return out;
57176
57453
  }
57177
57454
 
57455
+ // src/server.ts
57456
+ init_complex_bugs();
57457
+
57178
57458
  // src/guidance.ts
57179
57459
  var SINK_DIRECTIVES = {
57180
57460
  sql_execution: "Validate the SQL injection live with strike_verify (use a unique MARKER and a NEGATIVE CONTROL that stays inert), or read_tool_manual for sqlmap. Only a reflected marker + inert control = confirmed.",
@@ -57393,11 +57673,15 @@ function dataRefresh() {
57393
57673
  };
57394
57674
  }
57395
57675
 
57676
+ // src/server.ts
57677
+ init_verify_harness();
57678
+
57396
57679
  // src/benchmark.ts
57397
57680
  import { readFileSync as readFileSync19, readdirSync as readdirSync5, writeFileSync as writeFileSync6, rmSync as rmSync3, mkdirSync as mkdirSync7 } from "node:fs";
57398
57681
  import { join as join18 } from "node:path";
57399
57682
  import { fileURLToPath as fileURLToPath10 } from "node:url";
57400
57683
  import { tmpdir as tmpdir3, homedir as homedir8 } from "node:os";
57684
+ init_complex_bugs();
57401
57685
  var ROOT5 = join18(fileURLToPath10(new URL(".", import.meta.url)), "..");
57402
57686
  var CORPUS = join18(ROOT5, "bench", "corpus");
57403
57687
  var CAPTURED_CORPUS_DIR = process.env.BLITZSTRIKE_CORPUS_DIR ?? join18(homedir8(), ".blitzstrike", "corpus");
@@ -57693,8 +57977,13 @@ function openCount() {
57693
57977
  return loadObligations().filter((o) => o.status === "open").length;
57694
57978
  }
57695
57979
 
57980
+ // src/server.ts
57981
+ init_security_state();
57982
+ init_framework_security();
57983
+
57696
57984
  // src/variant-scan.ts
57697
57985
  init_scanner();
57986
+ init_complex_bugs();
57698
57987
  import { readFileSync as readFileSync22 } from "node:fs";
57699
57988
 
57700
57989
  // src/cross-file.ts
@@ -58592,6 +58881,7 @@ function analyzeJwt(token, opts = {}) {
58592
58881
  // src/enterprise-benchmark.ts
58593
58882
  import { existsSync as existsSync11, readFileSync as readFileSync23, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
58594
58883
  import { join as join20, resolve } from "node:path";
58884
+ init_complex_bugs();
58595
58885
  var ROOT6 = resolve(process.cwd(), "bench", "enterprise");
58596
58886
  var ENTERPRISE_FIXTURES = [
58597
58887
  { id: "php-laravel", dir: join20(ROOT6, "php-laravel"), expected_vulns: 4 },
@@ -59245,6 +59535,13 @@ ${statusLine}` : statusLine },
59245
59535
  }, async () => {
59246
59536
  return { content: [{ type: "text", text: JSON.stringify(dataRefresh()) }] };
59247
59537
  });
59538
+ server.registerTool("run_verification", {
59539
+ title: "Detector verification harness",
59540
+ description: "VERIFY: run every detector against its positive (must fire) + negative (must stay silent) fixture and report TP/FP/FN per detector. This is the reproducible 'battle-tested' proof — run it to confirm the detectors still pass after any change.",
59541
+ inputSchema: {}
59542
+ }, async () => {
59543
+ return { content: [{ type: "text", text: JSON.stringify(runVerificationHarness()) }] };
59544
+ });
59248
59545
  server.registerTool("fofa_search", {
59249
59546
  title: "FOFA asset search",
59250
59547
  description: "STRIKE: search the FOFA asset index (needs FOFA_EMAIL + FOFA_KEY).",
@@ -60613,6 +60910,7 @@ Usage:
60613
60910
  blitzstrike install-tools --category recon --dry-run Preview / filter
60614
60911
  blitzstrike sync-data Fetch heavy datasets (payloads + templates) on-demand
60615
60912
  blitzstrike update Check for a newer version + refresh the data cache
60913
+ blitzstrike verify Run every detector against TP/FP fixtures + report
60616
60914
  blitzstrike version Print version
60617
60915
  blitzstrike --help This help`;
60618
60916
  }
@@ -60664,6 +60962,14 @@ async function main() {
60664
60962
  console.log(` data: payloads ${r.data_sync.payloads.files} files, templates ${r.data_sync.templates.files} files ${r.data_sync.payloads.synced ? "(refreshed)" : ""}`);
60665
60963
  return;
60666
60964
  }
60965
+ if (args[0] === "verify") {
60966
+ await Promise.resolve().then(() => init_verify_harness());
60967
+ console.log(verificationReportText());
60968
+ const r = runVerificationHarness();
60969
+ if (r.failed > 0)
60970
+ process.exit(1);
60971
+ return;
60972
+ }
60667
60973
  if (args[0] === "serve") {
60668
60974
  await serve();
60669
60975
  return;