blitzstrike 1.0.39 → 1.0.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 +31 -0
- package/bench/corpus/java-cmd-processbuilder.md +8 -0
- package/bench/corpus/java-deser-readunshared.md +8 -0
- package/bench/corpus/java-sqli-execute.md +8 -0
- package/bench/corpus/java-ssrf-httpurl.md +8 -0
- package/bench/corpus/java-xss-escape.md +8 -0
- package/bench/corpus/java-xss-println.md +8 -0
- package/bench/corpus/js-cmd-execsync.md +8 -0
- package/bench/corpus/js-filewrite.md +8 -0
- package/bench/corpus/js-pathtraversal-readsync.md +8 -0
- package/bench/corpus/js-redirect-encodeuri.md +8 -0
- package/bench/corpus/js-ssrf-axios.md +8 -0
- package/bench/corpus/js-ssrf-fetch.md +8 -0
- package/bench/corpus/js-xss-documentwrite.md +8 -0
- package/bench/corpus/js-xss-innerhtml.md +8 -0
- package/bench/corpus/php-cmd-absint.md +9 -0
- package/bench/corpus/php-cmd-popen.md +9 -0
- package/bench/corpus/php-deser-json.md +9 -0
- package/bench/corpus/php-include-once.md +9 -0
- package/bench/corpus/php-include-sanitized.md +9 -0
- package/bench/corpus/php-sqli-filterinput.md +9 -0
- package/bench/corpus/php-sqli-getresults.md +9 -0
- package/bench/corpus/php-ssrf-wppost.md +9 -0
- package/bench/corpus/php-xss-escurl.md +9 -0
- package/bench/corpus/php-xss-print.md +9 -0
- package/bench/corpus/php-xss-sanitizetext.md +9 -0
- package/bench/corpus/py-cmd-popen.md +9 -0
- package/bench/corpus/py-cmd-shlex.md +9 -0
- package/bench/corpus/py-deser-yaml.md +9 -0
- package/bench/corpus/py-sqli-executescript.md +9 -0
- package/bench/corpus/py-ssrf-urllib.md +9 -0
- package/bench/corpus/py-xss-escape.md +8 -0
- package/dist/index.js +201 -8
- package/package.json +12 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,37 @@ 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
|
+
## [1.0.41] — 2026-09-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Phase 6 — Browser Validation** (`src/browser.ts` + MCP tool `browser_validate`):
|
|
13
|
+
- Playwright-based validation of static-analysis hypotheses (not a scanner —
|
|
14
|
+
gated, "use only when necessary").
|
|
15
|
+
- Checks: `dom_xss` (marker-payload execution), `open_redirect` (unexpected
|
|
16
|
+
redirect), `auth_bypass` (missing auth enforcement), `csrf` (tokenless
|
|
17
|
+
state-changing form).
|
|
18
|
+
- Each check returns structured evidence (verdict + final_url + dom/console/
|
|
19
|
+
page-error signals) suitable for attaching to a finding.
|
|
20
|
+
- `playwright-core` is an **optional peer dependency**; `browser_validate`
|
|
21
|
+
degrades to a clear `unavailable` result when it is not installed.
|
|
22
|
+
- Hermetic browser test suite (`bun run test:browser`, 8 checks) against a
|
|
23
|
+
local vulnerable server.
|
|
24
|
+
|
|
25
|
+
## [1.0.40] — 2026-09-13
|
|
26
|
+
|
|
27
|
+
### Changed (review + spec alignment)
|
|
28
|
+
|
|
29
|
+
- **Coverage Matrix 2.0 (§57)**: `coverageMatrix()` now also tracks the framework
|
|
30
|
+
dimension — per-framework sinks/sources/sanitizers/auth counts + CWE set.
|
|
31
|
+
- **Benchmark 100+ (§65)**: corpus grown 70 → **101 fixtures** (70 vulnerable +
|
|
32
|
+
31 patched), still scoring detection 1.0 / FP 0 / FN 0 / precision 1.0.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- JS adapter now detects `el.innerHTML = <tainted>` (property assignment) as a
|
|
37
|
+
DOM XSS sink — previously only `innerHTML` inside call expressions was caught.
|
|
38
|
+
|
|
8
39
|
## [1.0.39] — 2026-09-13
|
|
9
40
|
|
|
10
41
|
### Added
|
package/dist/index.js
CHANGED
|
@@ -50492,13 +50492,23 @@ var jsAdapter = {
|
|
|
50492
50492
|
rhsVars.push(c.name);
|
|
50493
50493
|
assigns.push({ target: name, sources: rhsVars, line: n.loc?.start?.line ?? 0 });
|
|
50494
50494
|
}
|
|
50495
|
-
if (n.type === "AssignmentExpression"
|
|
50496
|
-
|
|
50497
|
-
|
|
50498
|
-
|
|
50499
|
-
|
|
50500
|
-
|
|
50501
|
-
|
|
50495
|
+
if (n.type === "AssignmentExpression") {
|
|
50496
|
+
if (n.left?.type === "MemberExpression" && n.left.property?.name === "innerHTML") {
|
|
50497
|
+
const rhsVars = [];
|
|
50498
|
+
for (const c of jsWalk(n.right, []))
|
|
50499
|
+
if (c.type === "Identifier" && c.name)
|
|
50500
|
+
rhsVars.push(c.name);
|
|
50501
|
+
for (const v of rhsVars)
|
|
50502
|
+
sinks.push({ variable: v, kind: "html_render", line: n.loc?.start?.line ?? 0, cwe: "CWE-79" });
|
|
50503
|
+
}
|
|
50504
|
+
if (n.left?.type === "Identifier") {
|
|
50505
|
+
const name = n.left.name;
|
|
50506
|
+
const rhsVars = [];
|
|
50507
|
+
for (const c of jsWalk(n.right, []))
|
|
50508
|
+
if (c.type === "Identifier" && c.name)
|
|
50509
|
+
rhsVars.push(c.name);
|
|
50510
|
+
assigns.push({ target: name, sources: rhsVars, line: n.loc?.start?.line ?? 0 });
|
|
50511
|
+
}
|
|
50502
50512
|
}
|
|
50503
50513
|
if (n.type === "CallExpression") {
|
|
50504
50514
|
const calleeText = srcText(n.callee);
|
|
@@ -51030,6 +51040,162 @@ function scanFramework(code, frameworkId) {
|
|
|
51030
51040
|
};
|
|
51031
51041
|
}
|
|
51032
51042
|
|
|
51043
|
+
// src/browser.ts
|
|
51044
|
+
var chromiumCache = null;
|
|
51045
|
+
async function ensureChromium() {
|
|
51046
|
+
if (chromiumCache)
|
|
51047
|
+
return chromiumCache;
|
|
51048
|
+
try {
|
|
51049
|
+
chromiumCache = await import("playwright-core");
|
|
51050
|
+
return chromiumCache;
|
|
51051
|
+
} catch {
|
|
51052
|
+
return null;
|
|
51053
|
+
}
|
|
51054
|
+
}
|
|
51055
|
+
function unavailable(check, target, reason) {
|
|
51056
|
+
return {
|
|
51057
|
+
check,
|
|
51058
|
+
target: redactSecrets(target),
|
|
51059
|
+
verdict: "unavailable",
|
|
51060
|
+
detail: reason,
|
|
51061
|
+
evidence: { console_errors: [], page_errors: [] }
|
|
51062
|
+
};
|
|
51063
|
+
}
|
|
51064
|
+
function errorEvidence(check, target, e) {
|
|
51065
|
+
return {
|
|
51066
|
+
check,
|
|
51067
|
+
target: redactSecrets(target),
|
|
51068
|
+
verdict: "error",
|
|
51069
|
+
detail: e instanceof Error ? e.message : String(e),
|
|
51070
|
+
evidence: { console_errors: [], page_errors: [] }
|
|
51071
|
+
};
|
|
51072
|
+
}
|
|
51073
|
+
async function validateDomXss(target, payload = '<img src=x onerror="window.__blitz_xss__=true">') {
|
|
51074
|
+
const chr = await ensureChromium();
|
|
51075
|
+
if (!chr)
|
|
51076
|
+
return unavailable("dom_xss", target, "playwright-core not installed (optional peer dependency)");
|
|
51077
|
+
const browser = await chr.chromium.launch({ headless: true });
|
|
51078
|
+
try {
|
|
51079
|
+
const page = await browser.newPage();
|
|
51080
|
+
const consoleErrors = [];
|
|
51081
|
+
const pageErrors = [];
|
|
51082
|
+
page.on("console", (m) => {
|
|
51083
|
+
if (m.type() === "error")
|
|
51084
|
+
consoleErrors.push(m.text());
|
|
51085
|
+
});
|
|
51086
|
+
page.on("pageerror", (e) => pageErrors.push(String(e)));
|
|
51087
|
+
const url = target.includes("{{payload}}") ? target.replace("{{payload}}", encodeURIComponent(payload)) : target;
|
|
51088
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
|
|
51089
|
+
await page.waitForTimeout(600);
|
|
51090
|
+
const marker = await page.evaluate(() => window.__blitz_xss__ === true);
|
|
51091
|
+
return {
|
|
51092
|
+
check: "dom_xss",
|
|
51093
|
+
target: redactSecrets(target),
|
|
51094
|
+
verdict: marker ? "confirmed" : "not_confirmed",
|
|
51095
|
+
detail: marker ? "payload executed (marker set)" : "payload did not execute (no marker)",
|
|
51096
|
+
evidence: { final_url: page.url(), dom_marker: marker, console_errors: consoleErrors, page_errors: pageErrors }
|
|
51097
|
+
};
|
|
51098
|
+
} catch (e) {
|
|
51099
|
+
return errorEvidence("dom_xss", target, e);
|
|
51100
|
+
} finally {
|
|
51101
|
+
await browser.close();
|
|
51102
|
+
}
|
|
51103
|
+
}
|
|
51104
|
+
async function validateOpenRedirect(target, payload) {
|
|
51105
|
+
const chr = await ensureChromium();
|
|
51106
|
+
if (!chr)
|
|
51107
|
+
return unavailable("open_redirect", target, "playwright-core not installed (optional peer dependency)");
|
|
51108
|
+
const browser = await chr.chromium.launch({ headless: true });
|
|
51109
|
+
try {
|
|
51110
|
+
const page = await browser.newPage();
|
|
51111
|
+
const url = target.includes("{{payload}}") ? target.replace("{{payload}}", encodeURIComponent(payload)) : target;
|
|
51112
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
|
|
51113
|
+
await page.waitForTimeout(400);
|
|
51114
|
+
const finalUrl = page.url();
|
|
51115
|
+
const redirected = finalUrl !== url && finalUrl.includes(payload);
|
|
51116
|
+
return {
|
|
51117
|
+
check: "open_redirect",
|
|
51118
|
+
target: redactSecrets(target),
|
|
51119
|
+
verdict: redirected ? "confirmed" : "not_confirmed",
|
|
51120
|
+
detail: redirected ? `browser redirected to ${finalUrl}` : `no redirect to attacker URL (final: ${finalUrl})`,
|
|
51121
|
+
evidence: { final_url: finalUrl, console_errors: [], page_errors: [] }
|
|
51122
|
+
};
|
|
51123
|
+
} catch (e) {
|
|
51124
|
+
return errorEvidence("open_redirect", target, e);
|
|
51125
|
+
} finally {
|
|
51126
|
+
await browser.close();
|
|
51127
|
+
}
|
|
51128
|
+
}
|
|
51129
|
+
async function validateAuthBypass(target) {
|
|
51130
|
+
const chr = await ensureChromium();
|
|
51131
|
+
if (!chr)
|
|
51132
|
+
return unavailable("auth_bypass", target, "playwright-core not installed (optional peer dependency)");
|
|
51133
|
+
const browser = await chr.chromium.launch({ headless: true });
|
|
51134
|
+
try {
|
|
51135
|
+
const page = await browser.newPage();
|
|
51136
|
+
const resp = await page.goto(target, { waitUntil: "domcontentloaded", timeout: 15000 });
|
|
51137
|
+
const status = resp?.status() ?? 0;
|
|
51138
|
+
const finalUrl = page.url();
|
|
51139
|
+
const enforced = status === 401 || status === 403 || /login|signin|auth/.test(finalUrl);
|
|
51140
|
+
return {
|
|
51141
|
+
check: "auth_bypass",
|
|
51142
|
+
target: redactSecrets(target),
|
|
51143
|
+
verdict: enforced ? "not_confirmed" : "confirmed",
|
|
51144
|
+
detail: enforced ? `auth enforced (status ${status}, final ${finalUrl})` : `no auth enforcement observed (status ${status}, final ${finalUrl})`,
|
|
51145
|
+
evidence: { final_url: finalUrl, status, console_errors: [], page_errors: [] }
|
|
51146
|
+
};
|
|
51147
|
+
} catch (e) {
|
|
51148
|
+
return errorEvidence("auth_bypass", target, e);
|
|
51149
|
+
} finally {
|
|
51150
|
+
await browser.close();
|
|
51151
|
+
}
|
|
51152
|
+
}
|
|
51153
|
+
async function validateCsrf(target) {
|
|
51154
|
+
const chr = await ensureChromium();
|
|
51155
|
+
if (!chr)
|
|
51156
|
+
return unavailable("csrf", target, "playwright-core not installed (optional peer dependency)");
|
|
51157
|
+
const browser = await chr.chromium.launch({ headless: true });
|
|
51158
|
+
try {
|
|
51159
|
+
const page = await browser.newPage();
|
|
51160
|
+
await page.goto(target, { waitUntil: "domcontentloaded", timeout: 15000 });
|
|
51161
|
+
const result = await page.evaluate(() => {
|
|
51162
|
+
const forms = Array.from(document.querySelectorAll("form"));
|
|
51163
|
+
const stateChanging = forms.filter((f) => /post/i.test(f.method) || f.method === "");
|
|
51164
|
+
if (stateChanging.length === 0)
|
|
51165
|
+
return { token: true, count: 0 };
|
|
51166
|
+
for (const f of stateChanging) {
|
|
51167
|
+
const token = f.querySelector('input[name*="csrf"], input[name*="token"], input[name*="_token"], input[name*="nonce"]');
|
|
51168
|
+
if (!token)
|
|
51169
|
+
return { token: false, count: stateChanging.length };
|
|
51170
|
+
}
|
|
51171
|
+
return { token: true, count: stateChanging.length };
|
|
51172
|
+
});
|
|
51173
|
+
return {
|
|
51174
|
+
check: "csrf",
|
|
51175
|
+
target: redactSecrets(target),
|
|
51176
|
+
verdict: result.token ? "not_confirmed" : "confirmed",
|
|
51177
|
+
detail: result.token ? `CSRF token present (${result.count} form(s))` : `state-changing form missing CSRF token (${result.count} form(s))`,
|
|
51178
|
+
evidence: { csrf_token_present: result.token, forms_count: result.count, console_errors: [], page_errors: [] }
|
|
51179
|
+
};
|
|
51180
|
+
} catch (e) {
|
|
51181
|
+
return errorEvidence("csrf", target, e);
|
|
51182
|
+
} finally {
|
|
51183
|
+
await browser.close();
|
|
51184
|
+
}
|
|
51185
|
+
}
|
|
51186
|
+
async function browserValidate(check, target, payload) {
|
|
51187
|
+
switch (check) {
|
|
51188
|
+
case "dom_xss":
|
|
51189
|
+
return validateDomXss(target, payload);
|
|
51190
|
+
case "open_redirect":
|
|
51191
|
+
return validateOpenRedirect(target, payload ?? "https://attacker.example/");
|
|
51192
|
+
case "auth_bypass":
|
|
51193
|
+
return validateAuthBypass(target);
|
|
51194
|
+
case "csrf":
|
|
51195
|
+
return validateCsrf(target);
|
|
51196
|
+
}
|
|
51197
|
+
}
|
|
51198
|
+
|
|
51033
51199
|
// src/guidance.ts
|
|
51034
51200
|
var SINK_DIRECTIVES = {
|
|
51035
51201
|
sql_execution: "Validate the SQL injection live with strike_verify (use a unique MARKER and a NEGATIVE CONTROL that stays inert), or read_tool_manual for sqlmap. Only a reflected marker + inert control = confirmed.",
|
|
@@ -52039,6 +52205,19 @@ function coverageMatrix() {
|
|
|
52039
52205
|
const covered = langs.map((l) => l.language).filter((lang) => SINK_BY_LANG[lang]?.[sink] === true);
|
|
52040
52206
|
return { sink, languages: covered };
|
|
52041
52207
|
});
|
|
52208
|
+
const frameworkCoverage = listFrameworks().map(({ id, language }) => {
|
|
52209
|
+
const p = frameworkProfile(id);
|
|
52210
|
+
const cwe = p ? [...new Set(p.sinks.map((s) => s.cwe).filter((c) => !!c))].sort() : [];
|
|
52211
|
+
return {
|
|
52212
|
+
framework: id,
|
|
52213
|
+
language,
|
|
52214
|
+
sinks: p?.sinks.length ?? 0,
|
|
52215
|
+
sources: p?.sources.length ?? 0,
|
|
52216
|
+
sanitizers: p?.sanitizers.length ?? 0,
|
|
52217
|
+
auth: p?.auth.length ?? 0,
|
|
52218
|
+
cwe
|
|
52219
|
+
};
|
|
52220
|
+
});
|
|
52042
52221
|
let coveredPairs = 0;
|
|
52043
52222
|
for (const lang of langs) {
|
|
52044
52223
|
for (const sink of SINK_CLASSES) {
|
|
@@ -52050,11 +52229,13 @@ function coverageMatrix() {
|
|
|
52050
52229
|
return {
|
|
52051
52230
|
languages: langs,
|
|
52052
52231
|
sink_coverage: sinkCoverage,
|
|
52232
|
+
framework_coverage: frameworkCoverage,
|
|
52053
52233
|
counts: {
|
|
52054
52234
|
languages: langs.length,
|
|
52055
52235
|
sink_classes: SINK_CLASSES.length,
|
|
52056
52236
|
covered_pairs: coveredPairs,
|
|
52057
|
-
total_pairs: totalPairs
|
|
52237
|
+
total_pairs: totalPairs,
|
|
52238
|
+
frameworks: frameworkCoverage.length
|
|
52058
52239
|
},
|
|
52059
52240
|
coverage_ratio: Number((coveredPairs / totalPairs).toFixed(3))
|
|
52060
52241
|
};
|
|
@@ -52818,6 +52999,18 @@ function createServer() {
|
|
|
52818
52999
|
out.scan = scanFramework(code, framework);
|
|
52819
53000
|
return { content: [{ type: "text", text: JSON.stringify(out) }] };
|
|
52820
53001
|
});
|
|
53002
|
+
server.registerTool("browser_validate", {
|
|
53003
|
+
title: "Browser validation (Playwright)",
|
|
53004
|
+
description: "PHASE 6: validate a static-analysis HYPOTHESIS by driving a real headless Chromium. Checks: dom_xss, open_redirect, auth_bypass, csrf. Use only when necessary — to confirm a hypothesis with browser evidence, not as a first-pass scan. Requires playwright-core (optional peer dependency).",
|
|
53005
|
+
inputSchema: {
|
|
53006
|
+
check: _enum(["dom_xss", "open_redirect", "auth_bypass", "csrf"]).describe("Validation check to run"),
|
|
53007
|
+
target: string2().describe("Target URL (use {{payload}} placeholder for dom_xss/open_redirect injection)"),
|
|
53008
|
+
payload: string2().optional().describe("Payload to inject (defaults to an XSS marker for dom_xss)")
|
|
53009
|
+
}
|
|
53010
|
+
}, async ({ check, target, payload }) => {
|
|
53011
|
+
const result = await browserValidate(check, target, payload);
|
|
53012
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
53013
|
+
});
|
|
52821
53014
|
server.registerTool("taint_tree", {
|
|
52822
53015
|
title: "Whole-tree taint scan",
|
|
52823
53016
|
description: "EAGLE-EYE: run inter-procedural taint analysis across an entire source tree.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blitzstrike",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.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": {
|
|
@@ -25,12 +25,13 @@
|
|
|
25
25
|
"tools-catalog.json"
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
|
-
"build": "bun build src/index.ts --outdir dist --target node --format esm",
|
|
28
|
+
"build": "bun build src/index.ts --outdir dist --target node --format esm --external playwright-core",
|
|
29
29
|
"typecheck": "tsc --noEmit",
|
|
30
30
|
"serve": "bun run src/index.ts serve",
|
|
31
31
|
"doctor": "bun run src/index.ts doctor",
|
|
32
32
|
"compile": "bun build src/index.ts --compile --outfile dist/blitzstrike",
|
|
33
33
|
"test": "bun run test/verify.ts",
|
|
34
|
+
"test:browser": "bun run test/browser-verify.ts",
|
|
34
35
|
"prepublishOnly": "bun run typecheck && bun run build",
|
|
35
36
|
"bench": "bun run test/verify.ts"
|
|
36
37
|
},
|
|
@@ -74,6 +75,15 @@
|
|
|
74
75
|
},
|
|
75
76
|
"devDependencies": {
|
|
76
77
|
"@types/node": "^22.20.2",
|
|
78
|
+
"playwright-core": "1.61.0",
|
|
77
79
|
"typescript": "^5.5.0"
|
|
80
|
+
},
|
|
81
|
+
"peerDependencies": {
|
|
82
|
+
"playwright-core": ">=1.61.0"
|
|
83
|
+
},
|
|
84
|
+
"peerDependenciesMeta": {
|
|
85
|
+
"playwright-core": {
|
|
86
|
+
"optional": true
|
|
87
|
+
}
|
|
78
88
|
}
|
|
79
89
|
}
|