blitzstrike 2.4.22 → 2.4.23

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,20 @@ 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.23] — 2026-09-14
9
+
10
+ ### Precision — false-positive reduction + per-detector benchmark
11
+
12
+ - Fixed 6 false-positive patterns in the heuristic detectors (complex-bugs +
13
+ route-confusion): canonicalized include (`basename`/`realpath`), whitelist
14
+ lookup (`$allowed[$p]`), batch-forwarding WITH an auth re-check, anchored
15
+ validation regex (`preg_match('/^[a-z]+$/')`), constrained SSRF (fixed
16
+ `scheme://` host literal), and URL literals in the path-confusion detector.
17
+ - Extended the benchmark to measure per-detector precision/recall/F1 (taint +
18
+ complex_bugs + route_confusion) with 28 new labelled fixtures.
19
+ - Corpus now at 137 cases: **precision 1.0 / recall 1.0 / F1 1.0, 0 FP, 0 FN**
20
+ across all three detectors.
21
+
8
22
  ## [2.4.22] — 2026-09-14
9
23
 
10
24
  ### Added — on-disk report persistence (reports/ folder)
@@ -0,0 +1,9 @@
1
+ - id: cb-crlf-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ header("Location: /home");
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: cb-crlf-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ header("Location: " . $_GET['url']);
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: cb-deser-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ $data = json_decode($_GET['data']);
9
+ ```
@@ -0,0 +1,11 @@
1
+ - id: cb-deser-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ $data = $_GET['data'];
9
+ $obj = unserialize($data);
10
+ class X { function __destruct() { system('id'); } }
11
+ ```
@@ -0,0 +1,9 @@
1
+ - id: cb-massassign-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ extract($_POST, EXTR_SKIP);
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: cb-massassign-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ extract($_REQUEST);
9
+ ```
@@ -0,0 +1,10 @@
1
+ - id: cb-pathconf-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ $f = basename($_GET['file']);
9
+ echo file_get_contents($f);
10
+ ```
@@ -0,0 +1,10 @@
1
+ - id: cb-pathconf-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ $f = $_GET['file'];
9
+ echo file_get_contents($f);
10
+ ```
@@ -0,0 +1,8 @@
1
+ - id: cb-proto-safe
2
+ - language: javascript
3
+ - vulnerable: false
4
+ - detector: complex_bugs
5
+
6
+ ```js
7
+ const x = Object.assign({}, { a: 1 });
8
+ ```
@@ -0,0 +1,8 @@
1
+ - id: cb-proto-vuln
2
+ - language: javascript
3
+ - vulnerable: true
4
+ - detector: complex_bugs
5
+
6
+ ```js
7
+ const x = Object.assign({}, req.body);
8
+ ```
@@ -0,0 +1,9 @@
1
+ - id: cb-ssrf-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ echo file_get_contents("https://api.example.com/" . $_GET['path']);
9
+ ```
@@ -0,0 +1,10 @@
1
+ - id: cb-ssrf-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ $url = $_GET['url'];
9
+ echo file_get_contents($url);
10
+ ```
@@ -0,0 +1,8 @@
1
+ - id: cb-ssti-safe
2
+ - language: javascript
3
+ - vulnerable: false
4
+ - detector: complex_bugs
5
+
6
+ ```js
7
+ const html = _.template("<h1>static</h1>");
8
+ ```
@@ -0,0 +1,8 @@
1
+ - id: cb-ssti-vuln
2
+ - language: javascript
3
+ - vulnerable: true
4
+ - detector: complex_bugs
5
+
6
+ ```js
7
+ const html = ejs.render(req.query.template);
8
+ ```
@@ -0,0 +1,9 @@
1
+ - id: cb-typejuggle-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ if ($_GET['password'] === $secret) { echo 'ok'; }
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: cb-typejuggle-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ if ($_GET['password'] == $secret) { echo 'ok'; }
9
+ ```
@@ -0,0 +1,11 @@
1
+ - id: cb-xxe-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ $xml = $_GET['xml'];
9
+ libxml_disable_entity_loader(true);
10
+ $doc = simplexml_load_string($xml);
11
+ ```
@@ -0,0 +1,10 @@
1
+ - id: cb-xxe-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ $xml = $_GET['xml'];
9
+ $doc = simplexml_load_string($xml);
10
+ ```
@@ -0,0 +1,9 @@
1
+ - id: rc-batch-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: route_confusion
5
+
6
+ ```php
7
+ <?php
8
+ foreach ($requests as $r) { if (!authorize($r)) continue; handle($r); }
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: rc-batch-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: route_confusion
5
+
6
+ ```php
7
+ <?php
8
+ foreach ($requests as $r) { forward($r); }
9
+ ```
@@ -0,0 +1,10 @@
1
+ - id: rc-dispatch-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: route_confusion
5
+
6
+ ```php
7
+ <?php
8
+ $cb = $_GET['cb'];
9
+ call_user_func($map[$cb]);
10
+ ```
@@ -0,0 +1,9 @@
1
+ - id: rc-dispatch-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: route_confusion
5
+
6
+ ```php
7
+ <?php
8
+ call_user_func($_GET['callback']);
9
+ ```
@@ -0,0 +1,10 @@
1
+ - id: rc-include-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: route_confusion
5
+
6
+ ```php
7
+ <?php
8
+ $f = basename($_GET['f']);
9
+ include($f);
10
+ ```
@@ -0,0 +1,9 @@
1
+ - id: rc-include-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: route_confusion
5
+
6
+ ```php
7
+ <?php
8
+ include($_GET['page']);
9
+ ```
@@ -0,0 +1,10 @@
1
+ - id: rc-method-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: route_confusion
5
+
6
+ ```php
7
+ <?php
8
+ $m = $_GET['m'];
9
+ $obj->{$allowed[$m]}();
10
+ ```
@@ -0,0 +1,10 @@
1
+ - id: rc-method-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: route_confusion
5
+
6
+ ```php
7
+ <?php
8
+ $m = $_GET['m'];
9
+ $obj->$m();
10
+ ```
@@ -0,0 +1,10 @@
1
+ - id: rc-normalize-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: route_confusion
5
+
6
+ ```php
7
+ <?php
8
+ $path = $_GET['p'];
9
+ if (preg_match("/^[a-z]+$/", $path)) route($path);
10
+ ```
@@ -0,0 +1,10 @@
1
+ - id: rc-normalize-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: route_confusion
5
+
6
+ ```php
7
+ <?php
8
+ $path = $_GET['p'];
9
+ if (preg_match("/^\/admin/", $path)) route($path);
10
+ ```
package/dist/index.js CHANGED
@@ -55630,6 +55630,8 @@ var DYNAMIC_METHOD_RE = /(->|::)\s*\$[A-Za-z_]/g;
55630
55630
  var BATCH_LOOP_RE = /\bforeach\s*\([^)]*(requests|batch|sub_requests|operations)\b/gi;
55631
55631
  var ROUTE_MATCH_RE = /\b(preg_match|preg_match_all|strpos|stripos|str_contains)\s*\(/i;
55632
55632
  var NORMALIZERS = /\b(strtolower|strtoupper|rtrim|urldecode|rawurldecode|parse_url|normalize|canonical)\b/i;
55633
+ var WHITELIST_LOOKUP = /\$[A-Za-z_]\w*\s*\[\s*\$[A-Za-z_]\w*\s*\]/;
55634
+ var CANONICALIZE = /\b(basename|realpath|normalize|canonical|resolve|toRealPath)\s*\(/i;
55633
55635
  function detectDynamicDispatch(code, file, out) {
55634
55636
  const ls = lines(code);
55635
55637
  DYNAMIC_DISPATCH_RE.lastIndex = 0;
@@ -55641,6 +55643,8 @@ function detectDynamicDispatch(code, file, out) {
55641
55643
  `);
55642
55644
  if (!userControlled(l) && !userControlled(scope))
55643
55645
  continue;
55646
+ if (WHITELIST_LOOKUP.test(l) || WHITELIST_LOOKUP.test(scope))
55647
+ continue;
55644
55648
  out.push({
55645
55649
  file,
55646
55650
  line: ln,
@@ -55663,6 +55667,8 @@ function detectDynamicMethod(code, file, out) {
55663
55667
  `);
55664
55668
  if (!userControlled(l) && !userControlled(scope))
55665
55669
  continue;
55670
+ if (WHITELIST_LOOKUP.test(l) || WHITELIST_LOOKUP.test(scope))
55671
+ continue;
55666
55672
  out.push({
55667
55673
  file,
55668
55674
  line: ln,
@@ -55685,6 +55691,11 @@ function detectDynamicInclude(code, file, out) {
55685
55691
  `);
55686
55692
  if (!userControlled(l) && !userControlled(scope))
55687
55693
  continue;
55694
+ if (CANONICALIZE.test(scope + `
55695
+ ` + l))
55696
+ continue;
55697
+ if (WHITELIST_LOOKUP.test(l) || WHITELIST_LOOKUP.test(scope))
55698
+ continue;
55688
55699
  out.push({
55689
55700
  file,
55690
55701
  line: ln,
@@ -55707,14 +55718,16 @@ function detectBatchForwarding(code, file, out) {
55707
55718
  if (!forwards)
55708
55719
  continue;
55709
55720
  const rechecks = /\b(current_user_can|permission_callback|check_ajax_referer|authorize|authorise|is_user_logged_in)\b/i.test(body);
55721
+ if (rechecks)
55722
+ continue;
55710
55723
  out.push({
55711
55724
  file,
55712
55725
  line: ln,
55713
55726
  type: "batch_forwarding",
55714
55727
  category: "Route confusion (batch/proxy forwarding)",
55715
- severity: rechecks ? "medium" : "high",
55728
+ severity: "high",
55716
55729
  evidence: lines(code)[ln - 1]?.trim() ?? "",
55717
- detail: rechecks ? "Batch/proxy loop forwards to inner handlers — auth is re-checked in scope, but verify it applies per-route." : "Batch/proxy loop forwards to inner handlers with no per-route authorization re-check — an attacker may reach a protected handler."
55730
+ detail: "Batch/proxy loop forwards to inner handlers with no per-route authorization re-check — an attacker may reach a protected handler."
55718
55731
  });
55719
55732
  }
55720
55733
  }
@@ -55730,6 +55743,8 @@ function detectRouteNormalization(code, file, out) {
55730
55743
  continue;
55731
55744
  if (NORMALIZERS.test(l) || NORMALIZERS.test(scope))
55732
55745
  continue;
55746
+ if (/preg_match\s*\(\s*["'][^"']*\^[^"']*\$[^"']*["']/.test(l))
55747
+ continue;
55733
55748
  out.push({
55734
55749
  file,
55735
55750
  line: i + 1,
@@ -55927,6 +55942,12 @@ function detectPathConfusion(code, file, out) {
55927
55942
  if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
55928
55943
  continue;
55929
55944
  if (/\b(basename|realpath|canonicalPath|getCanonicalPath|normalize|resolve|toRealPath)\s*\(/i.test(scope + `
55945
+ ` + l))
55946
+ continue;
55947
+ if (/\$[A-Za-z_]\w*\s*\[\s*\$[A-Za-z_]\w*\s*\]/.test(scope + `
55948
+ ` + l))
55949
+ continue;
55950
+ if (/["'][^"']*:\/\/[^"']*["']/.test(scope + `
55930
55951
  ` + l))
55931
55952
  continue;
55932
55953
  out.push({
@@ -55952,6 +55973,9 @@ function detectSsrF(code, file, out) {
55952
55973
  if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
55953
55974
  continue;
55954
55975
  if (/(allowlist|whitelist|in_array|preg_match|parse_url|startsWith\(["']https?|endsWith\(["']|basename|realpath|normalize)/i.test(scope + `
55976
+ ` + l))
55977
+ continue;
55978
+ if (/["'][^"']*:\/\/[^"']*["']/.test(scope + `
55955
55979
  ` + l))
55956
55980
  continue;
55957
55981
  out.push({
@@ -56088,11 +56112,12 @@ function parseCorpusFile(path) {
56088
56112
  let language = "php";
56089
56113
  let vulnerable = true;
56090
56114
  let sinkType;
56115
+ let detector;
56091
56116
  let code = "";
56092
56117
  let inCode = false;
56093
56118
  let codeLang = "";
56094
56119
  for (const line of lines) {
56095
- const m = line.match(/^\s*-\s*(id|language|vulnerable|sink_type)\s*:\s*(.+)$/i);
56120
+ const m = line.match(/^\s*-\s*(id|language|vulnerable|sink_type|detector)\s*:\s*(.+)$/i);
56096
56121
  if (!inCode && m) {
56097
56122
  const key = m[1].toLowerCase();
56098
56123
  const val = m[2].trim();
@@ -56104,6 +56129,8 @@ function parseCorpusFile(path) {
56104
56129
  vulnerable = val === "true";
56105
56130
  else if (key === "sink_type")
56106
56131
  sinkType = val;
56132
+ else if (key === "detector")
56133
+ detector = val;
56107
56134
  continue;
56108
56135
  }
56109
56136
  const start = line.match(/^```(\w*)/);
@@ -56120,7 +56147,7 @@ function parseCorpusFile(path) {
56120
56147
  }
56121
56148
  if (!id)
56122
56149
  id = path.split("/").pop().replace(/\.md$/, "");
56123
- return { id, language: language || codeLang || "php", vulnerable, sink_type: sinkType, code: code.trim() };
56150
+ return { id, language: language || codeLang || "php", vulnerable, sink_type: sinkType, detector, code: code.trim() };
56124
56151
  }
56125
56152
  function loadCorpus() {
56126
56153
  let entries = [];
@@ -56138,6 +56165,18 @@ function loadCorpus() {
56138
56165
  return entries;
56139
56166
  }
56140
56167
  function detect(entry) {
56168
+ if (entry.detector === "route_confusion") {
56169
+ const f = detectRouteConfusion(entry.code, "bench." + entry.language);
56170
+ if (f.length > 0)
56171
+ return { detected: true, sinkKind: f[0].type };
56172
+ return { detected: false };
56173
+ }
56174
+ if (entry.detector === "complex_bugs") {
56175
+ const f = detectComplexBugs(entry.code, "bench." + entry.language);
56176
+ if (f.length > 0)
56177
+ return { detected: true, sinkKind: f[0].type };
56178
+ return { detected: false };
56179
+ }
56141
56180
  const lang = entry.language;
56142
56181
  if (lang === "php") {
56143
56182
  const tmp = join15(tmpdir3(), `blitzstrike-bench-${entry.id}-${process.pid}.php`);
@@ -56169,6 +56208,19 @@ function runBenchmark() {
56169
56208
  const detectionRate = tp + fn > 0 ? tp / (tp + fn) : 0;
56170
56209
  const falsePositiveRate = fp + tn > 0 ? fp / (fp + tn) : 0;
56171
56210
  const precision = tp + fp > 0 ? tp / (tp + fp) : 0;
56211
+ const byDetector = {};
56212
+ for (const r of results) {
56213
+ const d = r.entry.detector ?? "taint";
56214
+ byDetector[d] = byDetector[d] ?? { tp: 0, tn: 0, fp: 0, fn: 0, total: 0 };
56215
+ byDetector[d][r.outcome] += 1;
56216
+ byDetector[d].total += 1;
56217
+ }
56218
+ const detectorMetrics = Object.fromEntries(Object.entries(byDetector).map(([d, m]) => {
56219
+ const p = m.tp + m.fp > 0 ? m.tp / (m.tp + m.fp) : 0;
56220
+ const rec = m.tp + m.fn > 0 ? m.tp / (m.tp + m.fn) : 0;
56221
+ const f1 = p + rec > 0 ? 2 * p * rec / (p + rec) : 0;
56222
+ return [d, { ...m, precision: Number(p.toFixed(3)), recall: Number(rec.toFixed(3)), f1: Number(f1.toFixed(3)) }];
56223
+ }));
56172
56224
  return {
56173
56225
  total_cases: total,
56174
56226
  tp,
@@ -56178,7 +56230,8 @@ function runBenchmark() {
56178
56230
  detection_rate: Number(detectionRate.toFixed(3)),
56179
56231
  false_positive_rate: Number(falsePositiveRate.toFixed(3)),
56180
56232
  precision: Number(precision.toFixed(3)),
56181
- results: results.map((r) => ({ id: r.entry.id, language: r.entry.language, outcome: r.outcome, sink: r.sink_kind }))
56233
+ by_detector: detectorMetrics,
56234
+ results: results.map((r) => ({ id: r.entry.id, language: r.entry.language, detector: r.entry.detector ?? "taint", outcome: r.outcome, sink: r.sink_kind }))
56182
56235
  };
56183
56236
  }
56184
56237
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blitzstrike",
3
- "version": "2.4.22",
3
+ "version": "2.4.23",
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": {