blitzstrike 2.4.45 → 2.4.47

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/CHANGELOG.md CHANGED
@@ -5,6 +5,34 @@ All notable changes to Blitz Strike are documented here.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
+ ## [2.4.47] — 2026-09-14
9
+
10
+ ### Changed — WordPress integration coverage is now COMPLETE (class-method + direct handlers)
11
+
12
+ - The WP authz/nonce detector previously only matched STRING handlers
13
+ (`add_action('wp_ajax_x','fn')`), missing the `array($this,'method')` /
14
+ `array('Class','method')` form that dominates modern class-based plugins.
15
+ Now resolves any handler form via `handlerName()` + `functionBody()`.
16
+ - Added raw request-handler detection: `add_action('init'|'admin_init'|
17
+ 'wp_loaded'|'template_redirect', ...)` that reads request input directly —
18
+ flagged for missing nonce (CSRF) and missing capability (privileged ops).
19
+ - Auth surface widened (`is_admin`, `is_super_admin`, `current_user_can_edit_post`,
20
+ `wp_doing_ajax`). Benchmark: elementor 0→11, forminator ~3→90, learnpress 1→11
21
+ authz/nonce findings (class-method handlers now visible).
22
+
23
+ ## [2.4.46] — 2026-09-14
24
+
25
+ ### Added — SQL injection (raw string-interpolated query) detector
26
+
27
+ - `detectSqlInjection` (in complex-bugs.ts): flags a SQL sink (`$wpdb->get_results/
28
+ get_col/get_var/get_row/query`, `->query`, `DB::unprepared`, `->whereRaw`,
29
+ `->selectRaw`) whose query is built by string concatenation (`. $var`) and NOT
30
+ `->prepare()`d — targeting the CROSS-FILE case the per-file taint engine cannot
31
+ see (source in a caller). Surfaces the raw-concat sink as a lead to trace back.
32
+ - Enterprise fixture `wp-batch-route` corrected to 2 vulns (route confusion +
33
+ the raw SQLi in `get_users`) — the new detector surfaced a real, previously
34
+ uncounted vuln.
35
+
8
36
  ## [2.4.45] — 2026-09-14
9
37
 
10
38
  ### Added — File Upload → RCE detector (from plugin benchmark)
package/dist/index.js CHANGED
@@ -56471,43 +56471,55 @@ function functionBody(code, name) {
56471
56471
  }
56472
56472
  return code.slice(open);
56473
56473
  }
56474
+ function handlerName(expr) {
56475
+ const quoted = expr.match(/['"]([A-Za-z_][A-Za-z0-9_]*)['"]/g);
56476
+ if (quoted && quoted.length > 0)
56477
+ return quoted[quoted.length - 1].replace(/['"]/g, "");
56478
+ return null;
56479
+ }
56474
56480
  var INDIRECT_AUTH = /\b[a-zA-Z0-9_]*(auth|permission|cap|nonce|can_|check|verify|allow)[a-zA-Z0-9_]*\s*\(/;
56481
+ var AUTH_RE = /current_user_can|is_user_logged_in|is_admin\b|is_super_admin|current_user_can_edit_post|authorize|wp_doing_ajax/;
56482
+ var NONCE_RE = /wp_verify_nonce|check_ajax_referer|check_admin_referer|verify_nonce/;
56475
56483
  function detectWpAuthz(code, file, out) {
56476
- const ajaxRe = /add_action\s*\(\s*['"](wp_ajax(?:_nopriv)?_\w+)['"]\s*,\s*['"](\w+)['"]/g;
56484
+ const ajaxRe = /add_action\s*\(\s*['"](wp_ajax(?:_nopriv)?_\w+)['"]\s*,\s*(.+?)\s*\)/g;
56477
56485
  let m;
56478
56486
  while ((m = ajaxRe.exec(code)) !== null) {
56479
56487
  const hook = m[1];
56480
- const handler = m[2];
56488
+ const handler = handlerName(m[2]);
56489
+ if (!handler)
56490
+ continue;
56481
56491
  const nopriv = hook.startsWith("wp_ajax_nopriv_");
56482
56492
  const body = functionBody(code, handler);
56483
56493
  if (!body)
56484
56494
  continue;
56485
- const hasCap = /current_user_can|is_user_logged_in|authorize/.test(body) || INDIRECT_AUTH.test(body);
56486
- const hasNonce = /wp_verify_nonce|check_ajax_referer|check_admin_referer|verify_nonce/.test(body);
56495
+ const hasCap = AUTH_RE.test(body) || INDIRECT_AUTH.test(body);
56496
+ const hasNonce = NONCE_RE.test(body);
56487
56497
  const line = lineNoOf(code, m.index);
56488
56498
  if (nopriv && !hasCap) {
56489
- out.push({ file, line, type: "missing_authz", category: "Missing capability check (unauthenticated AJAX handler)", severity: "high", evidence: `add_action('${hook}', '${handler}')`, detail: `Unauthenticated AJAX handler '${handler}()' performs no current_user_can()/authorization check — any unauthenticated visitor can trigger it (auth bypass / privilege escalation).` });
56499
+ 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 no current_user_can()/authorization check — any unauthenticated visitor can trigger it (auth bypass / privilege escalation).` });
56490
56500
  }
56491
56501
  if (!hasNonce) {
56492
- out.push({ file, line, type: "missing_nonce", category: "Missing nonce verification", severity: "medium", evidence: `add_action('${hook}', '${handler}')`, detail: `AJAX handler '${handler}()' does not verify a nonce (wp_verify_nonce / check_ajax_referer) — CSRF.` });
56502
+ out.push({ file, line, type: "missing_nonce", category: "Missing nonce verification", severity: "medium", evidence: `add_action('${hook}', ...)`, detail: `AJAX handler '${handler}()' does not verify a nonce (wp_verify_nonce / check_ajax_referer) — CSRF.` });
56493
56503
  }
56494
56504
  }
56495
- const adminRe = /add_action\s*\(\s*['"](admin_post(?:_nopriv)?_\w+)['"]\s*,\s*['"](\w+)['"]/g;
56505
+ const adminRe = /add_action\s*\(\s*['"](admin_post(?:_nopriv)?_\w+)['"]\s*,\s*(.+?)\s*\)/g;
56496
56506
  while ((m = adminRe.exec(code)) !== null) {
56497
56507
  const hook = m[1];
56498
- const handler = m[2];
56508
+ const handler = handlerName(m[2]);
56509
+ if (!handler)
56510
+ continue;
56499
56511
  const nopriv = hook.startsWith("admin_post_nopriv_");
56500
56512
  const body = functionBody(code, handler);
56501
56513
  if (!body)
56502
56514
  continue;
56503
- const hasCap = /current_user_can|is_user_logged_in|authorize/.test(body) || INDIRECT_AUTH.test(body);
56504
- const hasNonce = /wp_verify_nonce|check_admin_referer|verify_nonce/.test(body);
56515
+ const hasCap = AUTH_RE.test(body) || INDIRECT_AUTH.test(body);
56516
+ const hasNonce = NONCE_RE.test(body);
56505
56517
  const line = lineNoOf(code, m.index);
56506
56518
  if (nopriv && !hasCap) {
56507
- out.push({ file, line, type: "missing_authz", category: "Missing capability check (unauthenticated admin-post handler)", severity: "high", evidence: `add_action('${hook}', '${handler}')`, detail: `Unauthenticated admin-post handler '${handler}()' has no authorization check — auth bypass / privilege escalation.` });
56519
+ 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}()' has no authorization check — auth bypass / privilege escalation.` });
56508
56520
  }
56509
56521
  if (!hasNonce) {
56510
- out.push({ file, line, type: "missing_nonce", category: "Missing nonce verification", severity: "medium", evidence: `add_action('${hook}', '${handler}')`, detail: `admin-post handler '${handler}()' does not verify a nonce — CSRF.` });
56522
+ out.push({ file, line, type: "missing_nonce", category: "Missing nonce verification", severity: "medium", evidence: `add_action('${hook}', ...)`, detail: `admin-post handler '${handler}()' does not verify a nonce — CSRF.` });
56511
56523
  }
56512
56524
  }
56513
56525
  const restRe = /register_rest_route\s*\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]/g;
@@ -56518,6 +56530,27 @@ function detectWpAuthz(code, file, out) {
56518
56530
  continue;
56519
56531
  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).` });
56520
56532
  }
56533
+ const formRe = /add_action\s*\(\s*['"](init|admin_init|wp_loaded|template_redirect|parse_request|wp)['"]\s*,\s*(.+?)\s*\)/g;
56534
+ while ((m = formRe.exec(code)) !== null) {
56535
+ const hook = m[1];
56536
+ const handler = handlerName(m[2]);
56537
+ if (!handler)
56538
+ continue;
56539
+ const body = functionBody(code, handler);
56540
+ if (!body)
56541
+ continue;
56542
+ if (!/\$_(GET|POST|REQUEST|COOKIE|SERVER)\b|php:\/\/input|filter_input|file_get_contents\(['"]php:\/\/input/.test(body))
56543
+ continue;
56544
+ const hasCap = AUTH_RE.test(body) || INDIRECT_AUTH.test(body);
56545
+ const hasNonce = NONCE_RE.test(body);
56546
+ const line = lineNoOf(code, m.index);
56547
+ if (!hasNonce) {
56548
+ 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}') reads request input without nonce verification — CSRF.` });
56549
+ }
56550
+ if (!hasCap && /(update_option|add_option|delete_option|wp_insert_post|wp_update_post|wp_delete_post|update_user_meta|delete_user_meta|wp_set_current_user|wp_mail|wp_handle_upload|move_uploaded_file|wp_delete_user|add_user|wp_create_user|set_user_role|wp_redirect\s*\(\s*['"]admin)/.test(body)) {
56551
+ 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.` });
56552
+ }
56553
+ }
56521
56554
  }
56522
56555
  var ROUTE_DETECTORS = [
56523
56556
  { 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 },
@@ -56679,6 +56712,7 @@ function detectComplexBugs(code, file) {
56679
56712
  detectXxe(code, file, out);
56680
56713
  detectSsti(code, file, out);
56681
56714
  detectFileUploadRce(code, file, out);
56715
+ detectSqlInjection(code, file, out);
56682
56716
  out.push(...detectWrongSanitizer(code, file));
56683
56717
  out.push(...detectMissingAuthz(code, file));
56684
56718
  const seen = new Set;
@@ -56864,6 +56898,34 @@ function detectFileUploadRce(code, file, out) {
56864
56898
  });
56865
56899
  }
56866
56900
  }
56901
+ function detectSqlInjection(code, file, out) {
56902
+ const ls = lines2(code);
56903
+ const sinkRe = /\$wpdb->(get_results|get_col|get_var|get_row|query)\s*\(|->query\s*\(|DB::unprepared\s*\(|->whereRaw\s*\(|->selectRaw\s*\(/g;
56904
+ let m;
56905
+ while ((m = sinkRe.exec(code)) !== null) {
56906
+ const ln = lineNo2(code, m.index);
56907
+ const l = ls[ln - 1] ?? "";
56908
+ const scope = ls.slice(Math.max(0, ln - 6), ln + 1).join(`
56909
+ `);
56910
+ if (/->prepare\s*\(/.test(scope))
56911
+ continue;
56912
+ if (SOURCE_TOKENS3.test(scope))
56913
+ continue;
56914
+ const inlineConcat = /\.\s*\$[A-Za-z_]\w*/.test(l);
56915
+ const assignConcat = /(?:^|\n)\s*\$[A-Za-z_]\w*\s*(?:\.=|=)\s*[^;\n]*["'][^"']*["']\s*\.\s*\$[A-Za-z_]\w*/.test(scope);
56916
+ if (!inlineConcat && !assignConcat)
56917
+ continue;
56918
+ out.push({
56919
+ file,
56920
+ line: ln,
56921
+ type: "sql_injection",
56922
+ category: "SQL injection (raw string-interpolated query)",
56923
+ severity: "medium",
56924
+ evidence: l.trim(),
56925
+ 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)."
56926
+ });
56927
+ }
56928
+ }
56867
56929
 
56868
56930
  // src/guidance.ts
56869
56931
  var SINK_DIRECTIVES = {
@@ -57887,7 +57949,7 @@ var ENTERPRISE_FIXTURES = [
57887
57949
  { id: "js-express", dir: join18(ROOT6, "js-express"), expected_vulns: 2 },
57888
57950
  { id: "py-django", dir: join18(ROOT6, "py-django"), expected_vulns: 2 },
57889
57951
  { id: "java-spring", dir: join18(ROOT6, "java-spring"), expected_vulns: 2 },
57890
- { id: "wp-batch-route", dir: join18(ROOT6, "wp-batch-route"), expected_vulns: 1 },
57952
+ { id: "wp-batch-route", dir: join18(ROOT6, "wp-batch-route"), expected_vulns: 2 },
57891
57953
  { id: "php-complex", dir: join18(ROOT6, "php-complex"), expected_vulns: 4 }
57892
57954
  ];
57893
57955
  function sourceFiles(root) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blitzstrike",
3
- "version": "2.4.45",
3
+ "version": "2.4.47",
4
4
  "description": "Blitz Strike — a universal MCP security-audit toolbelt. BLITZ sweeps the attack surface, EAGLE-EYE traces source-to-sink, STRIKE verifies live. 57 attack chains, 130-tool catalog, intelligence data layer. One server, every agent.",
5
5
  "type": "module",
6
6
  "bin": {