blitzstrike 2.4.39 → 2.4.41

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,30 @@ 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.41] — 2026-09-14
9
+
10
+ ### Added — Blind Differential Oracle (calibration-based sink detection)
11
+
12
+ - New `src/blind-oracle.ts`: detect which sink a request parameter reaches
13
+ WITHOUT a malicious payload — calibration + differential. Arithmetic
14
+ identities (`id=1` vs `id=1+0`/`id=2-1`) reveal SQL/expression context; SSTI
15
+ arithmetic (`test${7*7}`) reveals template engines; a unique marker reveals
16
+ XSS reflection. Benign probes (numbers + operators), deterministic inference.
17
+ - New `blind_oracle` tool: WAF-safe blind detection for sql/ssti/xss.
18
+
19
+ ## [2.4.40] — 2026-09-14
20
+
21
+ ### Added — Web Cache Key Normalization Gap Fuzzer (live, black-box)
22
+
23
+ - New `src/cache-gap.ts`: fuzz an endpoint's path-normalization + static-
24
+ extension variants (`.css`, `..;/`, `%2f`, `%00`, `;`, case, …) to detect
25
+ web-cache deception / poisoning — where a cache keys on a static-looking path
26
+ but the origin serves private content.
27
+ - New `cache_gap` tool: fetch a baseline, compare body hash + cache headers per
28
+ variant, and flag only evidence-backed gaps (cache_deception / normalization_gap).
29
+ - Pure helpers `generateCacheVariants` + `classifyCacheResponse` for deterministic,
30
+ testable detection logic.
31
+
8
32
  ## [2.4.39] — 2026-09-14
9
33
 
10
34
  ### Added — Patch Reversal (1-day weaponization engine)
package/dist/index.js CHANGED
@@ -57367,6 +57367,262 @@ function functionBody2(code, name) {
57367
57367
  return code.slice(open);
57368
57368
  }
57369
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
+
57509
+ // src/blind-oracle.ts
57510
+ function arithmeticProbes(value) {
57511
+ const n = Number(value);
57512
+ if (Number.isNaN(n) || value.trim() === "")
57513
+ return [];
57514
+ return [
57515
+ { sink: "sql_arithmetic", name: "plus_zero", payload: `${n}+0` },
57516
+ { sink: "sql_arithmetic", name: "minus_zero", payload: `${n}-0` },
57517
+ { sink: "sql_arithmetic", name: "times_one", payload: `${n}*1` },
57518
+ { sink: "sql_coercion", name: "leading_zero", payload: `0${value}` },
57519
+ { sink: "sql_coercion", name: "float_form", payload: `${n}.0` }
57520
+ ];
57521
+ }
57522
+ function sstiProbes(value) {
57523
+ return [
57524
+ { sink: "ssti", name: "dollar_math", payload: `${value}\${7*7}` },
57525
+ { sink: "ssti", name: "hash_math", payload: `${value}#{7*7}` },
57526
+ { sink: "ssti", name: "mustache_math", payload: `${value}{{7*7}}` }
57527
+ ];
57528
+ }
57529
+ function xssProbe(value, marker) {
57530
+ return { sink: "xss", name: "marker", payload: `${value}${marker}` };
57531
+ }
57532
+ function classifyBlindProbe(input) {
57533
+ const same = input.bodyHash === input.baselineHash;
57534
+ let signal = false;
57535
+ let reason = "";
57536
+ if (input.sink === "sql_arithmetic" && same) {
57537
+ signal = true;
57538
+ reason = "arithmetic identity returned the SAME result as the bare value — the parameter is evaluated arithmetically (SQL/expression context).";
57539
+ } else if (input.sink === "sql_coercion" && same) {
57540
+ signal = true;
57541
+ reason = "type-coerced variant returned the SAME result — the parameter is normalized numerically (SQL typed column).";
57542
+ } else if (input.sink === "ssti" && !same && /49/.test(input.body)) {
57543
+ signal = true;
57544
+ reason = "the SSTI arithmetic (7*7) rendered as 49 in the response — the parameter reaches a template engine (SSTI).";
57545
+ } else if (input.sink === "xss" && input.marker && input.body.includes(input.marker)) {
57546
+ signal = true;
57547
+ reason = "the unique marker was reflected in the response — the parameter reaches HTML output (reflected).";
57548
+ }
57549
+ return { same_as_baseline: same, signal, reason };
57550
+ }
57551
+ async function blindOracle(baseUrl, param, value, opts = {}) {
57552
+ const base = baseUrl.replace(/\/+$/, "");
57553
+ const sinks = opts.sinks ?? ["sql", "ssti", "xss"];
57554
+ const marker = `zz${Math.random().toString(36).slice(2, 10)}zz`;
57555
+ const headers = { "User-Agent": "Mozilla/5.0 (blitzstrike oracle probe)" };
57556
+ const timeoutMs = opts.timeoutMs ?? 8000;
57557
+ const build = (payload) => `${base}?${encodeURIComponent(param)}=${encodeURIComponent(payload)}`;
57558
+ let baselineHash = "";
57559
+ let baselineSize = 0;
57560
+ let baselineStatus = 0;
57561
+ try {
57562
+ const r = await fetch(build(value), { headers, redirect: "manual", signal: AbortSignal.timeout(timeoutMs) });
57563
+ baselineStatus = r.status;
57564
+ const body = await r.text();
57565
+ baselineHash = sha256(body);
57566
+ baselineSize = body.length;
57567
+ } catch (e) {
57568
+ return { error: "baseline fetch failed", base_url: base, param, detail: String(e) };
57569
+ }
57570
+ const probes = [];
57571
+ if (sinks.includes("sql"))
57572
+ probes.push(...arithmeticProbes(value));
57573
+ if (sinks.includes("ssti"))
57574
+ probes.push(...sstiProbes(value));
57575
+ if (sinks.includes("xss"))
57576
+ probes.push(xssProbe(value, marker));
57577
+ const results = [];
57578
+ for (const p of probes) {
57579
+ const t0 = Date.now();
57580
+ try {
57581
+ const r = await fetch(build(p.payload), { headers, redirect: "manual", signal: AbortSignal.timeout(timeoutMs) });
57582
+ const timing = Date.now() - t0;
57583
+ const status = r.status;
57584
+ const body = await r.text();
57585
+ const bodyHash = sha256(body);
57586
+ const cls = classifyBlindProbe({
57587
+ baselineHash,
57588
+ baselineSize,
57589
+ bodyHash,
57590
+ size: body.length,
57591
+ sink: p.sink,
57592
+ name: p.name,
57593
+ body,
57594
+ marker
57595
+ });
57596
+ results.push({
57597
+ sink: p.sink,
57598
+ name: p.name,
57599
+ payload: p.payload,
57600
+ status,
57601
+ size: body.length,
57602
+ timing_ms: timing,
57603
+ body_hash: bodyHash.slice(0, 12),
57604
+ same_as_baseline: cls.same_as_baseline,
57605
+ signal: cls.signal,
57606
+ reason: cls.reason
57607
+ });
57608
+ } catch {
57609
+ results.push({ sink: p.sink, name: p.name, payload: p.payload, status: 0, size: 0, timing_ms: 0, body_hash: "", same_as_baseline: false, signal: false, reason: "fetch error/timeout" });
57610
+ }
57611
+ }
57612
+ const signals = results.filter((r) => r.signal === true);
57613
+ return {
57614
+ base_url: base,
57615
+ param,
57616
+ value,
57617
+ baseline: { status: baselineStatus, size: baselineSize, body_hash: baselineHash.slice(0, 16) },
57618
+ probes_tested: results.length,
57619
+ signals_found: signals.length,
57620
+ signals,
57621
+ all: results,
57622
+ interpretation: "A signal means the parameter reaches that sink (evidence: the differential). Verify live before reporting — the oracle is a detection hint, not proof."
57623
+ };
57624
+ }
57625
+
57370
57626
  // src/enterprise-benchmark.ts
57371
57627
  import { existsSync as existsSync11, readFileSync as readFileSync20, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
57372
57628
  import { join as join18, resolve } from "node:path";
@@ -58355,6 +58611,35 @@ ${statusLine}` : statusLine },
58355
58611
  return { content: [{ type: "text", text: JSON.stringify({ error: "provide code or path" }) }] };
58356
58612
  return { content: [{ type: "text", text: JSON.stringify(actionSurface(src)) }] };
58357
58613
  });
58614
+ server.registerTool("cache_gap", {
58615
+ title: "Web cache key normalization gap fuzzer",
58616
+ 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.",
58617
+ inputSchema: {
58618
+ base_url: string2().describe("Target base URL (e.g. https://target.com)"),
58619
+ endpoint: string2().describe("Endpoint path to probe (e.g. /account or /profile)"),
58620
+ max_variants: number2().optional().describe("Max variants to test (default 60)")
58621
+ }
58622
+ }, async ({ base_url, endpoint, max_variants }) => {
58623
+ return { content: [{ type: "text", text: JSON.stringify(await cacheGapScan(base_url, endpoint, { maxVariants: max_variants })) }] };
58624
+ });
58625
+ server.registerTool("blind_oracle", {
58626
+ title: "Blind differential oracle (calibration-based sink detection)",
58627
+ description: "LIVE (black-box, WAF-safe): detect which sink a request parameter reaches WITHOUT a malicious payload — calibration + differential. Arithmetic identities (id=1 vs id=1+0 / id=2-1) reveal SQL/expression context; SSTI arithmetic (test${7*7}) reveals template engines; a unique marker reveals XSS reflection. Benign probes (numbers + operators, no quotes/commands), deterministic byte-level inference.",
58628
+ inputSchema: {
58629
+ base_url: string2().describe("Target base URL"),
58630
+ param: string2().describe("Parameter name to probe (e.g. id)"),
58631
+ value: string2().describe("Baseline value for the parameter (e.g. 1)"),
58632
+ sinks: string2().optional().describe("JSON array of sinks to probe (default ['sql','ssti','xss'])")
58633
+ }
58634
+ }, async ({ base_url, param, value, sinks }) => {
58635
+ let s;
58636
+ try {
58637
+ s = sinks ? JSON.parse(sinks) : undefined;
58638
+ } catch {
58639
+ return { content: [{ type: "text", text: JSON.stringify({ error: "invalid sinks JSON" }) }] };
58640
+ }
58641
+ return { content: [{ type: "text", text: JSON.stringify(await blindOracle(base_url, param, value, { sinks: s })) }] };
58642
+ });
58358
58643
  server.registerTool("payload_lookup", {
58359
58644
  title: "Look up exploit payloads",
58360
58645
  description: "INTEL: find a payload collection (66 categories from PayloadsAllTheThings).",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blitzstrike",
3
- "version": "2.4.39",
3
+ "version": "2.4.41",
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": {