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 +14 -0
- package/bench/corpus/cb-crlf-safe.md +9 -0
- package/bench/corpus/cb-crlf-vuln.md +9 -0
- package/bench/corpus/cb-deser-safe.md +9 -0
- package/bench/corpus/cb-deser-vuln.md +11 -0
- package/bench/corpus/cb-massassign-safe.md +9 -0
- package/bench/corpus/cb-massassign-vuln.md +9 -0
- package/bench/corpus/cb-pathconf-safe.md +10 -0
- package/bench/corpus/cb-pathconf-vuln.md +10 -0
- package/bench/corpus/cb-proto-safe.md +8 -0
- package/bench/corpus/cb-proto-vuln.md +8 -0
- package/bench/corpus/cb-ssrf-safe.md +9 -0
- package/bench/corpus/cb-ssrf-vuln.md +10 -0
- package/bench/corpus/cb-ssti-safe.md +8 -0
- package/bench/corpus/cb-ssti-vuln.md +8 -0
- package/bench/corpus/cb-typejuggle-safe.md +9 -0
- package/bench/corpus/cb-typejuggle-vuln.md +9 -0
- package/bench/corpus/cb-xxe-safe.md +11 -0
- package/bench/corpus/cb-xxe-vuln.md +10 -0
- package/bench/corpus/rc-batch-safe.md +9 -0
- package/bench/corpus/rc-batch-vuln.md +9 -0
- package/bench/corpus/rc-dispatch-safe.md +10 -0
- package/bench/corpus/rc-dispatch-vuln.md +9 -0
- package/bench/corpus/rc-include-safe.md +10 -0
- package/bench/corpus/rc-include-vuln.md +9 -0
- package/bench/corpus/rc-method-safe.md +10 -0
- package/bench/corpus/rc-method-vuln.md +10 -0
- package/bench/corpus/rc-normalize-safe.md +10 -0
- package/bench/corpus/rc-normalize-vuln.md +10 -0
- package/dist/index.js +58 -5
- package/package.json +1 -1
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)
|
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:
|
|
55728
|
+
severity: "high",
|
|
55716
55729
|
evidence: lines(code)[ln - 1]?.trim() ?? "",
|
|
55717
|
-
detail:
|
|
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
|
-
|
|
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.
|
|
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": {
|