blitzstrike 2.4.38 → 2.4.40
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 +25 -0
- package/dist/index.js +273 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ 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.40] — 2026-09-14
|
|
9
|
+
|
|
10
|
+
### Added — Web Cache Key Normalization Gap Fuzzer (live, black-box)
|
|
11
|
+
|
|
12
|
+
- New `src/cache-gap.ts`: fuzz an endpoint's path-normalization + static-
|
|
13
|
+
extension variants (`.css`, `..;/`, `%2f`, `%00`, `;`, case, …) to detect
|
|
14
|
+
web-cache deception / poisoning — where a cache keys on a static-looking path
|
|
15
|
+
but the origin serves private content.
|
|
16
|
+
- New `cache_gap` tool: fetch a baseline, compare body hash + cache headers per
|
|
17
|
+
variant, and flag only evidence-backed gaps (cache_deception / normalization_gap).
|
|
18
|
+
- Pure helpers `generateCacheVariants` + `classifyCacheResponse` for deterministic,
|
|
19
|
+
testable detection logic.
|
|
20
|
+
|
|
21
|
+
## [2.4.39] — 2026-09-14
|
|
22
|
+
|
|
23
|
+
### Added — Patch Reversal (1-day weaponization engine)
|
|
24
|
+
|
|
25
|
+
- `analyzePatch` (in differential.ts): read a security patch (old vs new code)
|
|
26
|
+
and REVERSE it — detect what the patch ADDED (sanitizer, auth/nonce gate,
|
|
27
|
+
parameterized query, input validation) or REMOVED (a dangerous sink fed by
|
|
28
|
+
attacker input), and reconstruct the vulnerability in the OLD version.
|
|
29
|
+
- New `patch_analyze` tool: each reversal carries a DERIVED `mine_signature` to
|
|
30
|
+
sweep unpatched installs via `variant_scan(root, { signature })`.
|
|
31
|
+
- A cosmetic-only change (no security delta) correctly yields no reversal.
|
|
32
|
+
|
|
8
33
|
## [2.4.38] — 2026-09-14
|
|
9
34
|
|
|
10
35
|
### Added — Action-Name Heuristic Engine (what to look for WHERE)
|
package/dist/index.js
CHANGED
|
@@ -55613,6 +55613,110 @@ function analyzeGitDiff(repo, base = "HEAD~1", head = "HEAD") {
|
|
|
55613
55613
|
const security_sensitive = files.reduce((n, f) => n + f.result.changes.filter((c) => c.severity === "high").length, 0);
|
|
55614
55614
|
return { repo, base, head, files, total_changes, security_sensitive };
|
|
55615
55615
|
}
|
|
55616
|
+
var PATCH_RANK = { high: 0, medium: 1 };
|
|
55617
|
+
function analyzePatch(oldCode, newCode, language = "php") {
|
|
55618
|
+
const { added, removed } = diffLines(oldCode, newCode);
|
|
55619
|
+
const findings = [];
|
|
55620
|
+
for (const l of added) {
|
|
55621
|
+
const sans = findSanitizers(l.text);
|
|
55622
|
+
if (sans.length > 0) {
|
|
55623
|
+
findings.push({
|
|
55624
|
+
kind: "sanitizer_added",
|
|
55625
|
+
old_vuln: "raw attacker input reached a sink without escaping",
|
|
55626
|
+
new_fix: l.text.trim(),
|
|
55627
|
+
severity: "high",
|
|
55628
|
+
line: l.line,
|
|
55629
|
+
detail: `sanitizer ADDED: ${sans.map((s) => s.label).join(", ")} — the OLD version passed raw input to a sink (sanitization gap).`,
|
|
55630
|
+
signature: "patch:sanitizer_added",
|
|
55631
|
+
mine_signature: "complex_bugs:wrong_sanitizer"
|
|
55632
|
+
});
|
|
55633
|
+
}
|
|
55634
|
+
const gates = AUTH_GATES.filter((g) => l.text.includes(g));
|
|
55635
|
+
if (gates.length > 0) {
|
|
55636
|
+
findings.push({
|
|
55637
|
+
kind: "authorization_added",
|
|
55638
|
+
old_vuln: "missing authorization / nonce check",
|
|
55639
|
+
new_fix: l.text.trim(),
|
|
55640
|
+
severity: "high",
|
|
55641
|
+
line: l.line,
|
|
55642
|
+
detail: `authorization gate ADDED: ${gates.join(", ")} — the OLD version performed no auth/nonce check (auth bypass / CSRF).`,
|
|
55643
|
+
signature: "patch:authorization_added",
|
|
55644
|
+
mine_signature: "complex_bugs:missing_authz"
|
|
55645
|
+
});
|
|
55646
|
+
}
|
|
55647
|
+
if (/->prepare\s*\(|\bprepare\s*\(\s*["']/.test(l.text)) {
|
|
55648
|
+
findings.push({
|
|
55649
|
+
kind: "prepared_statement_added",
|
|
55650
|
+
old_vuln: "raw SQL string interpolation",
|
|
55651
|
+
new_fix: l.text.trim(),
|
|
55652
|
+
severity: "high",
|
|
55653
|
+
line: l.line,
|
|
55654
|
+
detail: "parameterized query ADDED — the OLD version concatenated user input into SQL (SQLi).",
|
|
55655
|
+
signature: "patch:prepared_statement_added",
|
|
55656
|
+
mine_signature: "complex_bugs:wrong_sanitizer"
|
|
55657
|
+
});
|
|
55658
|
+
}
|
|
55659
|
+
if (/preg_match\s*\(\s*["']\/\^|in_array\s*\(\s*\$|ctype_[a-z]+|is_numeric\s*\(|filter_var\s*\(/.test(l.text)) {
|
|
55660
|
+
findings.push({
|
|
55661
|
+
kind: "validation_added",
|
|
55662
|
+
old_vuln: "unvalidated / arbitrary input accepted",
|
|
55663
|
+
new_fix: l.text.trim(),
|
|
55664
|
+
severity: "medium",
|
|
55665
|
+
line: l.line,
|
|
55666
|
+
detail: "input validation ADDED — the OLD version accepted arbitrary input (type/format gap).",
|
|
55667
|
+
signature: "patch:validation_added",
|
|
55668
|
+
mine_signature: "complex_bugs:wrong_sanitizer"
|
|
55669
|
+
});
|
|
55670
|
+
}
|
|
55671
|
+
}
|
|
55672
|
+
for (const l of removed) {
|
|
55673
|
+
const sink = classifySink2(l.text);
|
|
55674
|
+
const source = classifySource(l.text);
|
|
55675
|
+
if (sink && source && source.attacker_controlled) {
|
|
55676
|
+
findings.push({
|
|
55677
|
+
kind: "sink_removed",
|
|
55678
|
+
old_vuln: `dangerous sink fed by attacker input: ${sink.category}`,
|
|
55679
|
+
new_fix: "(removed)",
|
|
55680
|
+
cwe: sink.cwe,
|
|
55681
|
+
severity: "high",
|
|
55682
|
+
line: l.line,
|
|
55683
|
+
detail: `sink REMOVED: ${l.text.trim()} (${sink.category}) — the OLD version executed this dangerous operation on attacker input.`,
|
|
55684
|
+
signature: `patch:sink_removed:${sink.category}`,
|
|
55685
|
+
mine_signature: `taint:${sink.category}`
|
|
55686
|
+
});
|
|
55687
|
+
}
|
|
55688
|
+
}
|
|
55689
|
+
findings.sort((a, b) => PATCH_RANK[a.severity] - PATCH_RANK[b.severity] || a.line - b.line);
|
|
55690
|
+
const count = (k) => findings.filter((f) => f.kind === k).length;
|
|
55691
|
+
return {
|
|
55692
|
+
language,
|
|
55693
|
+
reversal_findings: findings.map((f) => ({
|
|
55694
|
+
kind: f.kind,
|
|
55695
|
+
old_vuln: f.old_vuln,
|
|
55696
|
+
new_fix: f.new_fix,
|
|
55697
|
+
cwe: f.cwe,
|
|
55698
|
+
line: f.line,
|
|
55699
|
+
severity: f.severity,
|
|
55700
|
+
detail: f.detail,
|
|
55701
|
+
signature: f.signature,
|
|
55702
|
+
mine_signature: f.mine_signature
|
|
55703
|
+
})),
|
|
55704
|
+
summary: {
|
|
55705
|
+
sanitizer_added: count("sanitizer_added"),
|
|
55706
|
+
authorization_added: count("authorization_added"),
|
|
55707
|
+
prepared_statement_added: count("prepared_statement_added"),
|
|
55708
|
+
validation_added: count("validation_added"),
|
|
55709
|
+
sink_removed: count("sink_removed")
|
|
55710
|
+
},
|
|
55711
|
+
interpretation: "Each reversal is the vulnerability REVERSED from the patch: the OLD version had this bug, the NEW version fixed it. Sweep unpatched installs with variant_scan(root, { signature: mine_signature })."
|
|
55712
|
+
};
|
|
55713
|
+
}
|
|
55714
|
+
function analyzePatchFiles(oldPath, newPath, language) {
|
|
55715
|
+
const oldCode = readFileSync15(oldPath, "utf8");
|
|
55716
|
+
const newCode = readFileSync15(newPath, "utf8");
|
|
55717
|
+
const lang = language ?? languageOf(newPath);
|
|
55718
|
+
return analyzePatch(oldCode, newCode, lang);
|
|
55719
|
+
}
|
|
55616
55720
|
|
|
55617
55721
|
// src/frameworks.ts
|
|
55618
55722
|
import { readFileSync as readFileSync16, existsSync as existsSync8 } from "node:fs";
|
|
@@ -57263,6 +57367,145 @@ function functionBody2(code, name) {
|
|
|
57263
57367
|
return code.slice(open);
|
|
57264
57368
|
}
|
|
57265
57369
|
|
|
57370
|
+
// src/cache-gap.ts
|
|
57371
|
+
function generateCacheVariants(endpoint) {
|
|
57372
|
+
const e = endpoint.startsWith("/") ? endpoint : "/" + endpoint;
|
|
57373
|
+
const variants = new Set;
|
|
57374
|
+
const add = (s) => variants.add(s);
|
|
57375
|
+
const exts = [".css", ".js", ".png", ".jpg", ".jpeg", ".json", ".txt", ".ico", ".html", ".svg", ".woff", ".xml", ".gif"];
|
|
57376
|
+
for (const ext of exts) {
|
|
57377
|
+
add(`${e}${ext}`);
|
|
57378
|
+
add(`${e}/${ext.slice(1)}`);
|
|
57379
|
+
add(`${e}${ext}?x=1`);
|
|
57380
|
+
add(`${e}${ext};`);
|
|
57381
|
+
add(`${e}${ext}%3b`);
|
|
57382
|
+
}
|
|
57383
|
+
const suffix = [
|
|
57384
|
+
"/style.css",
|
|
57385
|
+
"/..;/style.css",
|
|
57386
|
+
"/.;/style.css",
|
|
57387
|
+
"/../style.css",
|
|
57388
|
+
"/%2e%2e/style.css",
|
|
57389
|
+
"/%2fstyle.css",
|
|
57390
|
+
"/;style.css",
|
|
57391
|
+
"/%3bstyle.css",
|
|
57392
|
+
"/./style.css",
|
|
57393
|
+
"//style.css",
|
|
57394
|
+
"/%00.style.css",
|
|
57395
|
+
"/style.css/.",
|
|
57396
|
+
"/style.css%2f",
|
|
57397
|
+
"/style.css%00",
|
|
57398
|
+
"/style.css%2e%2e"
|
|
57399
|
+
];
|
|
57400
|
+
for (const s of suffix)
|
|
57401
|
+
add(`${e}${s}`);
|
|
57402
|
+
add(e.replace(/^\//, "/%2f"));
|
|
57403
|
+
add(e.replace(/^\//, "//"));
|
|
57404
|
+
add(e.replace(/^\//, "/./"));
|
|
57405
|
+
add(e.replace(/^\//, "/%2e/"));
|
|
57406
|
+
add(e.replace(/^\//, "/..;/"));
|
|
57407
|
+
add(`${e}%00`);
|
|
57408
|
+
add(`${e};`);
|
|
57409
|
+
add(`${e}%3b`);
|
|
57410
|
+
add(`${e}?`);
|
|
57411
|
+
add(`${e}?x=1`);
|
|
57412
|
+
add(`${e}#`);
|
|
57413
|
+
add(e.toUpperCase());
|
|
57414
|
+
return [...variants].filter((v) => v !== e);
|
|
57415
|
+
}
|
|
57416
|
+
function classifyCacheResponse(input) {
|
|
57417
|
+
const same = input.bodyHash === input.baselineHash;
|
|
57418
|
+
const lower = Object.fromEntries(Object.entries(input.headers).map(([k, v]) => [k.toLowerCase(), v]));
|
|
57419
|
+
const cacheStatus = lower["x-cache"] ?? lower["cf-cache-status"] ?? lower["x-served-by"] ?? lower["x-cache-status"] ?? null;
|
|
57420
|
+
const cacheControl = lower["cache-control"] ?? "";
|
|
57421
|
+
const age = lower["age"];
|
|
57422
|
+
const cacheable = /public|max-age|s-maxage/.test(cacheControl) || age !== undefined || cacheStatus !== null && /hit/i.test(cacheStatus) || input.staticExtension;
|
|
57423
|
+
if (same && input.staticExtension && cacheable) {
|
|
57424
|
+
return {
|
|
57425
|
+
same_body: true,
|
|
57426
|
+
cacheable,
|
|
57427
|
+
cache_status: cacheStatus,
|
|
57428
|
+
gap: "cache_deception",
|
|
57429
|
+
detail: "the endpoint's content is returned for a static-extension path that is cacheable — a cache may store PRIVATE content under a PUBLIC static key (cache deception)."
|
|
57430
|
+
};
|
|
57431
|
+
}
|
|
57432
|
+
if (same && cacheStatus !== null && /hit/i.test(cacheStatus)) {
|
|
57433
|
+
return {
|
|
57434
|
+
same_body: true,
|
|
57435
|
+
cacheable,
|
|
57436
|
+
cache_status: cacheStatus,
|
|
57437
|
+
gap: "cache_deception",
|
|
57438
|
+
detail: `the endpoint's content is served from cache (${cacheStatus}) — the response is being cached and shared.`
|
|
57439
|
+
};
|
|
57440
|
+
}
|
|
57441
|
+
if (!same && input.staticExtension && input.status !== 200) {
|
|
57442
|
+
return {
|
|
57443
|
+
same_body: false,
|
|
57444
|
+
cacheable,
|
|
57445
|
+
cache_status: cacheStatus,
|
|
57446
|
+
gap: "normalization_gap",
|
|
57447
|
+
detail: "the static-extension variant resolves differently than the baseline (origin normalized the path) — cache key may diverge from the origin resource (normalization gap)."
|
|
57448
|
+
};
|
|
57449
|
+
}
|
|
57450
|
+
return { same_body: same, cacheable, cache_status: cacheStatus, gap: null, detail: "" };
|
|
57451
|
+
}
|
|
57452
|
+
function headersToRecord(h) {
|
|
57453
|
+
const out = {};
|
|
57454
|
+
h.forEach((v, k) => {
|
|
57455
|
+
out[k.toLowerCase()] = v;
|
|
57456
|
+
});
|
|
57457
|
+
return out;
|
|
57458
|
+
}
|
|
57459
|
+
function pickCacheHeaders(h) {
|
|
57460
|
+
const keys = ["cache-control", "age", "expires", "x-cache", "cf-cache-status", "x-served-by", "x-cache-status", "via", "etag", "last-modified", "vary"];
|
|
57461
|
+
const out = {};
|
|
57462
|
+
for (const k of keys)
|
|
57463
|
+
if (h[k] !== undefined)
|
|
57464
|
+
out[k] = h[k];
|
|
57465
|
+
return out;
|
|
57466
|
+
}
|
|
57467
|
+
async function cacheGapScan(baseUrl, endpoint, opts = {}) {
|
|
57468
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
57469
|
+
const endpointPath = endpoint.startsWith("/") ? endpoint : "/" + endpoint;
|
|
57470
|
+
const variants = generateCacheVariants(endpointPath).slice(0, opts.maxVariants ?? 60);
|
|
57471
|
+
const headers = { "User-Agent": "Mozilla/5.0 (blitzstrike cache probe)", Accept: "*/*" };
|
|
57472
|
+
let baselineHash = "";
|
|
57473
|
+
let baselineStatus = 0;
|
|
57474
|
+
let baselineHeaders = {};
|
|
57475
|
+
try {
|
|
57476
|
+
const resp = await fetch(base + endpointPath, { headers, redirect: "manual", signal: AbortSignal.timeout(opts.timeoutMs ?? 8000) });
|
|
57477
|
+
baselineStatus = resp.status;
|
|
57478
|
+
baselineHeaders = headersToRecord(resp.headers);
|
|
57479
|
+
baselineHash = sha256(await resp.text());
|
|
57480
|
+
} catch (e) {
|
|
57481
|
+
return { error: "baseline fetch failed", base_url: base, endpoint: endpointPath, detail: String(e) };
|
|
57482
|
+
}
|
|
57483
|
+
const results = [];
|
|
57484
|
+
for (const v of variants) {
|
|
57485
|
+
try {
|
|
57486
|
+
const resp = await fetch(base + v, { headers, redirect: "manual", signal: AbortSignal.timeout(opts.timeoutMs ?? 8000) });
|
|
57487
|
+
const status = resp.status;
|
|
57488
|
+
const hdrs = headersToRecord(resp.headers);
|
|
57489
|
+
const bodyHash = sha256(await resp.text());
|
|
57490
|
+
const staticExtension = /\.(css|js|png|jpe?g|json|txt|ico|html|svg|woff|xml|gif)([;?].*)?$/i.test(v);
|
|
57491
|
+
const cls = classifyCacheResponse({ baselineHash, bodyHash, status, headers: hdrs, staticExtension });
|
|
57492
|
+
results.push({ path: v, status, same_body: cls.same_body, cacheable: cls.cacheable, cache_status: cls.cache_status, gap: cls.gap, detail: cls.detail });
|
|
57493
|
+
} catch {
|
|
57494
|
+
results.push({ path: v, status: 0, same_body: false, cacheable: false, cache_status: null, gap: null, detail: "fetch error/timeout" });
|
|
57495
|
+
}
|
|
57496
|
+
}
|
|
57497
|
+
const gaps = results.filter((r) => r.gap !== null);
|
|
57498
|
+
return {
|
|
57499
|
+
base_url: base,
|
|
57500
|
+
endpoint: endpointPath,
|
|
57501
|
+
baseline: { status: baselineStatus, body_hash: baselineHash.slice(0, 16), cache_headers: pickCacheHeaders(baselineHeaders) },
|
|
57502
|
+
variants_tested: results.length,
|
|
57503
|
+
gaps_found: gaps.length,
|
|
57504
|
+
gaps,
|
|
57505
|
+
all: results
|
|
57506
|
+
};
|
|
57507
|
+
}
|
|
57508
|
+
|
|
57266
57509
|
// src/enterprise-benchmark.ts
|
|
57267
57510
|
import { existsSync as existsSync11, readFileSync as readFileSync20, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
|
|
57268
57511
|
import { join as join18, resolve } from "node:path";
|
|
@@ -58251,6 +58494,17 @@ ${statusLine}` : statusLine },
|
|
|
58251
58494
|
return { content: [{ type: "text", text: JSON.stringify({ error: "provide code or path" }) }] };
|
|
58252
58495
|
return { content: [{ type: "text", text: JSON.stringify(actionSurface(src)) }] };
|
|
58253
58496
|
});
|
|
58497
|
+
server.registerTool("cache_gap", {
|
|
58498
|
+
title: "Web cache key normalization gap fuzzer",
|
|
58499
|
+
description: "LIVE (black-box, no source code): fuzz an endpoint's path-normalization + static-extension variants to detect web-cache deception / poisoning — where a cache keys on a static-looking path but the origin serves private content. Deterministic + evidence-first: fetch a baseline, then compare body hash + cache headers per variant. High-impact class (cached private data, mass takeover) that is still under-hunted.",
|
|
58500
|
+
inputSchema: {
|
|
58501
|
+
base_url: string2().describe("Target base URL (e.g. https://target.com)"),
|
|
58502
|
+
endpoint: string2().describe("Endpoint path to probe (e.g. /account or /profile)"),
|
|
58503
|
+
max_variants: number2().optional().describe("Max variants to test (default 60)")
|
|
58504
|
+
}
|
|
58505
|
+
}, async ({ base_url, endpoint, max_variants }) => {
|
|
58506
|
+
return { content: [{ type: "text", text: JSON.stringify(await cacheGapScan(base_url, endpoint, { maxVariants: max_variants })) }] };
|
|
58507
|
+
});
|
|
58254
58508
|
server.registerTool("payload_lookup", {
|
|
58255
58509
|
title: "Look up exploit payloads",
|
|
58256
58510
|
description: "INTEL: find a payload collection (66 categories from PayloadsAllTheThings).",
|
|
@@ -58840,6 +59094,25 @@ ${statusLine}` : statusLine },
|
|
|
58840
59094
|
}
|
|
58841
59095
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
58842
59096
|
});
|
|
59097
|
+
server.registerTool("patch_analyze", {
|
|
59098
|
+
title: "Patch reversal (1-day weaponization)",
|
|
59099
|
+
description: "1-DAY: read a security patch (old vs new code) and REVERSE it — find what the patch ADDED to make the code safer (sanitizer, auth/nonce gate, parameterized query, validation) or REMOVED (a dangerous sink), and reconstruct the vulnerability in the OLD version. Each reversal carries a DERIVED mine_signature to sweep unpatched installs via variant_scan(root, { signature: mine_signature }).",
|
|
59100
|
+
inputSchema: {
|
|
59101
|
+
old_path: string2().optional().describe("Old file path"),
|
|
59102
|
+
new_path: string2().optional().describe("New file path"),
|
|
59103
|
+
old_code: string2().optional().describe("Old source code (when not using paths)"),
|
|
59104
|
+
new_code: string2().optional().describe("New source code (when not using paths)"),
|
|
59105
|
+
language: string2().optional().describe("Language (default php)")
|
|
59106
|
+
}
|
|
59107
|
+
}, async ({ old_path, new_path, old_code, new_code, language }) => {
|
|
59108
|
+
if (old_path && new_path) {
|
|
59109
|
+
return { content: [{ type: "text", text: JSON.stringify(analyzePatchFiles(old_path, new_path, language)) }] };
|
|
59110
|
+
}
|
|
59111
|
+
if (old_code !== undefined && new_code !== undefined) {
|
|
59112
|
+
return { content: [{ type: "text", text: JSON.stringify(analyzePatch(old_code, new_code, language ?? "php")) }] };
|
|
59113
|
+
}
|
|
59114
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "provide (old_path+new_path) or (old_code+new_code)" }) }] };
|
|
59115
|
+
});
|
|
58843
59116
|
server.registerTool("incremental_scan", {
|
|
58844
59117
|
title: "Incremental scan (CI)",
|
|
58845
59118
|
description: "EAGLE-EYE: analyze only the files changed between two git refs (default HEAD~1..HEAD) — the fast CI pass. Runs the taint + data-flow engine on just the changed source files and skips unchanged ones; the full-scan path (run_engagement / enrich_scan) remains authoritative.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blitzstrike",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.40",
|
|
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": {
|