blitzstrike 2.4.6 → 2.4.7

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,21 @@ 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.7] — 2026-09-14
9
+
10
+ ### Added — deterministic arbitrary-file-read verification (no bare hypotheses)
11
+
12
+ - New `verifyFileRead` (STRIKE): verifies a CWE-22 / path-traversal / LFI
13
+ hypothesis by reading a marker file (`/etc/passwd`) vs a non-existent
14
+ negative-control path, then comparing. Confirmed only when the marker returns
15
+ file content and the control does not — never from reasoning alone.
16
+ - Supports raw POST body (`bodyRaw`) and named query/body param injection.
17
+ - New MCP tool `verify_file_read` + chain hints `verify_file_read` /
18
+ `path_traversal` / `lfi` / `file_read` route to it; `strike_verify` now
19
+ auto-routes file-read findings to the file-read verifier.
20
+ - A gated endpoint (e.g. identical 500 auth wall) now returns an explicit
21
+ `unconfirmed` verdict with reason instead of leaving a bare hypothesis.
22
+
8
23
  ## [2.4.6] — 2026-09-14
9
24
 
10
25
  ### Added — chrome-devtools-mcp auto-install (npx zero-install fallback)
package/dist/index.js CHANGED
@@ -31291,6 +31291,53 @@ ${controlResp.body}` }]
31291
31291
  evidence
31292
31292
  };
31293
31293
  }
31294
+ function buildFileReadRequest(url, path, param, bodyRaw) {
31295
+ if (bodyRaw)
31296
+ return { url, data: path };
31297
+ return { url: inject(url, path, param ?? "path") };
31298
+ }
31299
+ async function verifyFileRead(input) {
31300
+ const method = input.method ?? "POST";
31301
+ const markerPath = input.markerPath ?? "/etc/passwd";
31302
+ const controlPath = `/bs-nonexistent-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
31303
+ const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
31304
+ const markerReq = buildFileReadRequest(input.url, markerPath, input.param, input.bodyRaw);
31305
+ const controlReq = buildFileReadRequest(input.url, controlPath, input.param, input.bodyRaw);
31306
+ const markerResp = await httpRequest(markerReq.url, method, markerReq.data, input.headers, timeoutMs);
31307
+ const controlResp = await httpRequest(controlReq.url, method, controlReq.data, input.headers, timeoutMs);
31308
+ const passwdRe = /(^|\n)root:[x*!][^:]*:0:0:/m;
31309
+ const markerFile = passwdRe.test(markerResp.body);
31310
+ const controlFile = passwdRe.test(controlResp.body);
31311
+ let status;
31312
+ let reason;
31313
+ if (markerResp.status === 0 && controlResp.status === 0) {
31314
+ status = "blocked";
31315
+ reason = "both marker and control requests failed — target unreachable";
31316
+ } else if (markerFile && !controlFile) {
31317
+ status = "confirmed";
31318
+ reason = "marker file (/etc/passwd) content returned while the non-existent negative control did not — arbitrary file read confirmed";
31319
+ } else if (markerFile && controlFile) {
31320
+ status = "unconfirmed";
31321
+ reason = "marker and negative control both returned passwd-like content — indistinguishable from a fixed response";
31322
+ } else if (markerResp.status === controlResp.status && markerResp.body === controlResp.body) {
31323
+ status = "unconfirmed";
31324
+ reason = `marker and control returned identical responses (HTTP ${markerResp.status}) — likely an auth/session gate; file read not reachable without credentials`;
31325
+ } else {
31326
+ status = "unconfirmed";
31327
+ reason = "marker file content not returned — arbitrary file read could not be demonstrated live";
31328
+ }
31329
+ return {
31330
+ status,
31331
+ file_read: markerFile && !controlFile,
31332
+ marker_file_content: markerFile,
31333
+ control_file_content: controlFile,
31334
+ marker_status: markerResp.status,
31335
+ control_status: controlResp.status,
31336
+ marker_preview: redactSecrets(markerResp.body.slice(0, 300)),
31337
+ control_preview: redactSecrets(controlResp.body.slice(0, 300)),
31338
+ reason
31339
+ };
31340
+ }
31294
31341
  function resolveFinding(finding, verdict) {
31295
31342
  let f = finding;
31296
31343
  if (f.status === "hypothesis") {
@@ -31826,6 +31873,10 @@ async function driveBurpMCP(baseUrl = DEFAULT_BURP_URL, timeoutMs = 1e4) {
31826
31873
  // src/chain-executor.ts
31827
31874
  var BLITZ_FINDING_TOOLS = {
31828
31875
  strike_verify: verifyFinding,
31876
+ verify_file_read: fileReadFinding,
31877
+ path_traversal: fileReadFinding,
31878
+ lfi: fileReadFinding,
31879
+ file_read: fileReadFinding,
31829
31880
  crack_hash: crackFinding,
31830
31881
  hashcat: crackFinding,
31831
31882
  hashid: crackFinding,
@@ -31859,9 +31910,15 @@ function findingText(f) {
31859
31910
  function findingUrl(f) {
31860
31911
  return (f.target?.host ?? "") + (f.target?.endpoint ?? "");
31861
31912
  }
31913
+ function isFileReadFinding(f) {
31914
+ const text = `${f.classification?.cwe ?? ""} ${f.classification?.cwe_name ?? ""} ${f.sink?.type ?? ""} ${f.chain?.id ?? ""} ${f.chain?.name ?? ""} ${f.title ?? ""}`.toLowerCase();
31915
+ return /(cwe-?22|path.?traversal|arbitrary.?file|file.?read|directory.?traversal|lfi|\btraversal\b)/.test(text);
31916
+ }
31862
31917
  async function verifyFinding(f) {
31863
31918
  const url = findingUrl(f);
31864
31919
  const param = f.source?.name ?? "q";
31920
+ if (isFileReadFinding(f))
31921
+ return fileReadFinding(f);
31865
31922
  const v = await strikeVerify({ url, method: "GET", param });
31866
31923
  return {
31867
31924
  status: v.status,
@@ -31871,6 +31928,16 @@ async function verifyFinding(f) {
31871
31928
  leaked_sinks: v.leaked_sinks ?? []
31872
31929
  };
31873
31930
  }
31931
+ async function fileReadFinding(f) {
31932
+ const url = findingUrl(f);
31933
+ if (!url)
31934
+ return { executed: false, note: "no target URL on finding" };
31935
+ const srcType = (f.source?.type ?? "").toLowerCase();
31936
+ const bodyRaw = /(body|post|raw)/.test(srcType);
31937
+ const method = bodyRaw ? "POST" : "GET";
31938
+ const verdict = await verifyFileRead({ url, method, bodyRaw, param: f.source?.name });
31939
+ return { verify_kind: "file_read", ...verdict };
31940
+ }
31874
31941
  async function crackFinding(f) {
31875
31942
  const text = findingText(f);
31876
31943
  const hashRe = /(\$2[aby]\$\d+\$[./A-Za-z0-9]{53}|[a-f0-9]{32}|[a-f0-9]{40}|[a-f0-9]{64})/g;
@@ -56279,6 +56346,45 @@ function createServer() {
56279
56346
  }]
56280
56347
  };
56281
56348
  });
56349
+ server.registerTool("verify_file_read", {
56350
+ title: "Verify arbitrary file read / path traversal (marker file vs negative control)",
56351
+ description: "STRIKE: deterministic verification of an arbitrary-file-read / path-traversal hypothesis (CWE-22). Reads a marker file (/etc/passwd) vs a non-existent negative-control path and compares: confirmed (marker returns file content, control does not) / unconfirmed (indistinguishable or gated by auth) / blocked (unreachable). Supports raw POST body (bodyRaw=true) or a named query/body param. Never leaves a bare hypothesis — always returns a verdict + reason.",
56352
+ inputSchema: {
56353
+ url: string2().describe("Target URL (e.g. the file-read endpoint)"),
56354
+ method: _enum(["GET", "POST"]).optional().describe("HTTP method (default POST)"),
56355
+ param: string2().optional().describe("Query/body param name that carries the path (default 'path')"),
56356
+ bodyRaw: boolean2().optional().describe("Treat the raw POST body as the filesystem path (default false)"),
56357
+ headers: string2().optional().describe("JSON object of extra headers"),
56358
+ markerPath: string2().optional().describe("Marker file to read (default /etc/passwd)"),
56359
+ timeout: number2().int().optional().describe("Timeout seconds (default 15)")
56360
+ }
56361
+ }, async ({ url, method, param, bodyRaw, headers, markerPath, timeout }) => {
56362
+ let hdrs = {};
56363
+ try {
56364
+ hdrs = headers ? JSON.parse(headers) : {};
56365
+ } catch {}
56366
+ const verdict = await verifyFileRead({
56367
+ url,
56368
+ method: method ?? "POST",
56369
+ param,
56370
+ bodyRaw: bodyRaw ?? false,
56371
+ headers: hdrs,
56372
+ markerPath,
56373
+ timeoutMs: timeout ? timeout * 1000 : undefined
56374
+ });
56375
+ return {
56376
+ content: [{
56377
+ type: "text",
56378
+ text: JSON.stringify({
56379
+ ...verdict,
56380
+ next_steps: [
56381
+ `Verdict: ${verdict.status} — ${verdict.reason}`,
56382
+ verdict.status === "confirmed" ? "Confirmed arbitrary file read. Record with finding_create + attach the marker/control evidence." : verdict.status === "blocked" ? "Blocked. Target unreachable — retry or re-check scope." : "Unconfirmed. The response is gated (auth/session) or the file is not read — obtain a low-priv session and re-run with a valid session, or mark the finding unconfirmed with this evidence."
56383
+ ]
56384
+ })
56385
+ }]
56386
+ };
56387
+ });
56282
56388
  server.registerTool("strike_resolve", {
56283
56389
  title: "Resolve a finding from a STRIKE verdict",
56284
56390
  description: "STRIKE+FINDINGS: take a canonical Finding and a STRIKE verdict, and advance the lifecycle end-to-end — hypothesis->validating->confirmed (marker reflected, control inert) or ->false_positive/blocked/unconfirmed. Returns the updated finding with evidence + recomputed confidence.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blitzstrike",
3
- "version": "2.4.6",
3
+ "version": "2.4.7",
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": {