blitzstrike 2.4.32 → 2.4.34

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,33 @@ 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.34] — 2026-09-14
9
+
10
+ ### Added — Framework Security Knowledge Graph (WordPress authz/nonce)
11
+
12
+ - New `src/framework-security.ts`: WordPress security primitives encoded as DATA
13
+ (auth, nonce, entry-point hooks, sanitizers), exposed via `framework_knowledge`.
14
+ - `detectMissingAuthz` (wired into `complex_scan`) finds missing capability /
15
+ nonce checks on WP entry points — `wp_ajax_*` / `wp_ajax_nopriv_*` /
16
+ `admin_post_*` / `register_rest_route` without `current_user_can`,
17
+ `wp_verify_nonce`, or `permission_callback` (auth bypass + CSRF + privesc,
18
+ the class that dominates real WordPress CVE reports).
19
+
20
+ ## [2.4.33] — 2026-09-14
21
+
22
+ ### Added — Security-State Lattice (beyond binary taint)
23
+
24
+ - New `src/security-state.ts`: models each value's SECURITY STATE
25
+ (clean < whitelisted < validated < sanitized(context) < tainted) instead of
26
+ binary tainted/not. A sink declares the minimum context it accepts.
27
+ - `detectWrongSanitizer` (wired into `complex_scan`/`blitz_file`) finds the
28
+ complex bug class binary taint misses: a value sanitized for context X
29
+ reaching a sink that needs Y (e.g. `sanitize_text_field()` into a SQL query),
30
+ and pseudo-sanitizers (`base64_encode()`/`trim()` misused as escaping).
31
+ - New `security_state` tool: deterministic verdict for a sanitizer flow.
32
+ - Data-driven tables (sanitizers, pseudo-sanitizers, sink requirements) —
33
+ the lattice is data, not guesses.
34
+
8
35
  ## [2.4.32] — 2026-09-14
9
36
 
10
37
  ### Removed
@@ -0,0 +1,15 @@
1
+ - id: cb-authz-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ add_action('wp_ajax_delete_user', 'safe_handler');
9
+ function safe_handler() {
10
+ check_ajax_referer('nonce');
11
+ if (!current_user_can('manage_options')) { wp_die('no'); }
12
+ $id = intval($_POST['id']);
13
+ $wpdb->query($wpdb->prepare("DELETE FROM users WHERE id=%d", $id));
14
+ }
15
+ ```
@@ -0,0 +1,13 @@
1
+ - id: cb-authz-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ add_action('wp_ajax_nopriv_delete_user', 'delete_user_handler');
9
+ function delete_user_handler() {
10
+ $id = $_POST['id'];
11
+ $wpdb->query("DELETE FROM users WHERE id=$id");
12
+ }
13
+ ```
@@ -0,0 +1,10 @@
1
+ - id: cb-wrongsanitizer-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ $name = esc_sql($_GET['name']);
9
+ $wpdb->query("SELECT * FROM users WHERE name = '$name'");
10
+ ```
@@ -0,0 +1,10 @@
1
+ - id: cb-wrongsanitizer-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ $name = sanitize_text_field($_GET['name']);
9
+ $wpdb->query("SELECT * FROM users WHERE name = '$name'");
10
+ ```
package/dist/index.js CHANGED
@@ -56078,8 +56078,360 @@ function detectRouteConfusion(code, file) {
56078
56078
  return out;
56079
56079
  }
56080
56080
 
56081
+ // src/security-state.ts
56082
+ var SANITIZERS2 = {
56083
+ esc_sql: { context: "sql", level: "sanitized" },
56084
+ addslashes: { context: "sql", level: "sanitized" },
56085
+ mysqli_real_escape_string: { context: "sql", level: "sanitized" },
56086
+ mysql_real_escape_string: { context: "sql", level: "sanitized" },
56087
+ pg_escape_string: { context: "sql", level: "sanitized" },
56088
+ sanitize_text_field: { context: "html", level: "sanitized" },
56089
+ sanitize_textarea_field: { context: "html", level: "sanitized" },
56090
+ esc_html: { context: "html", level: "sanitized" },
56091
+ esc_html__: { context: "html", level: "sanitized" },
56092
+ htmlspecialchars: { context: "html", level: "sanitized" },
56093
+ htmlentities: { context: "html", level: "sanitized" },
56094
+ wp_kses: { context: "html", level: "sanitized" },
56095
+ wp_kses_post: { context: "html", level: "sanitized" },
56096
+ wp_kses_data: { context: "html", level: "sanitized" },
56097
+ strip_tags: { context: "html", level: "sanitized" },
56098
+ esc_attr: { context: "html_attr", level: "sanitized" },
56099
+ esc_url: { context: "url", level: "sanitized" },
56100
+ esc_url_raw: { context: "url", level: "sanitized" },
56101
+ urlencode: { context: "url", level: "sanitized" },
56102
+ rawurlencode: { context: "url", level: "sanitized" },
56103
+ wp_json_encode: { context: "json", level: "sanitized" },
56104
+ json_encode: { context: "json", level: "sanitized" },
56105
+ absint: { context: "int", level: "validated" },
56106
+ intval: { context: "int", level: "validated" },
56107
+ sanitize_key: { context: "key", level: "validated" },
56108
+ sanitize_email: { context: "email", level: "validated" },
56109
+ sanitize_file_name: { context: "filename", level: "validated" },
56110
+ filter_var: { context: "validated", level: "validated" }
56111
+ };
56112
+ var PSEUDO_SANITIZERS = new Set([
56113
+ "trim",
56114
+ "ltrim",
56115
+ "rtrim",
56116
+ "strtolower",
56117
+ "strtoupper",
56118
+ "ucfirst",
56119
+ "ucwords",
56120
+ "base64_encode",
56121
+ "base64_decode",
56122
+ "urldecode",
56123
+ "rawurldecode",
56124
+ "stripslashes",
56125
+ "html_entity_decode",
56126
+ "htmlspecialchars_decode",
56127
+ "json_decode",
56128
+ "unserialize",
56129
+ "serialize",
56130
+ "substr",
56131
+ "mb_substr",
56132
+ "explode",
56133
+ "implode",
56134
+ "str_replace",
56135
+ "preg_replace",
56136
+ "nl2br",
56137
+ "strval",
56138
+ "number_format",
56139
+ "sprintf",
56140
+ "md5",
56141
+ "sha1",
56142
+ "hash",
56143
+ "crypt",
56144
+ "chr",
56145
+ "ord",
56146
+ "strrev",
56147
+ "str_repeat",
56148
+ "str_pad"
56149
+ ]);
56150
+ var SINK_REQUIREMENTS = {
56151
+ sql_execution: ["sql", "sql_param", "int"],
56152
+ code_execution: ["code"],
56153
+ command_execution: ["code"],
56154
+ deserialization: ["code"],
56155
+ file_operations: ["filename"],
56156
+ redirect: ["url"],
56157
+ html_render: ["html"]
56158
+ };
56159
+ var SOURCE_TOKENS2 = /\$_(GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|->(input|query|request)\(|\breq\.(query|body|params|headers|cookies)|\bgetParameter\(/;
56160
+ function analyzeSecurityState(input) {
56161
+ let state = "tainted";
56162
+ let context = null;
56163
+ for (const fn of input.sanitizers) {
56164
+ const s = SANITIZERS2[fn];
56165
+ if (!s)
56166
+ continue;
56167
+ if (s.level === "validated") {
56168
+ if (state === "tainted" || state === "sanitized") {
56169
+ state = "validated";
56170
+ context = s.context;
56171
+ }
56172
+ } else if (state === "tainted") {
56173
+ state = "sanitized";
56174
+ context = s.context;
56175
+ }
56176
+ }
56177
+ const required = SINK_REQUIREMENTS[input.sinkType] ?? [];
56178
+ if (required.length === 0) {
56179
+ return { verdict: "unknown", state, context, required, reason: `sink type '${input.sinkType}' not in the requirements table` };
56180
+ }
56181
+ if (state === "tainted") {
56182
+ return { verdict: "vulnerable", state, context, required, reason: `raw tainted input reaches a ${input.sinkType} sink with no sanitization` };
56183
+ }
56184
+ if (state === "validated") {
56185
+ return { verdict: "safe", state, context, required, reason: `input validated (${context}) — safe for any sink` };
56186
+ }
56187
+ if (state === "sanitized") {
56188
+ if (context && required.includes(context)) {
56189
+ return { verdict: "safe", state, context, required, reason: `sanitized for '${context}' — matches sink requirement ${required.join("/")}` };
56190
+ }
56191
+ return { verdict: "vulnerable", state, context, required, reason: `sanitized for '${context}' but this sink requires ${required.join("/")} — WRONG sanitizer (context mismatch)` };
56192
+ }
56193
+ return { verdict: "unknown", state, context, required, reason: "unhandled state" };
56194
+ }
56195
+ function sinkTypeOf(line) {
56196
+ if (/\$wpdb->(query|get_var|get_row|get_results|get_col)|mysqli_query\s*\(|mysql_query\s*\(|pg_query\s*\(|->query\s*\(/.test(line) && !/->prepare\s*\(/.test(line))
56197
+ return "sql_execution";
56198
+ if (/\b(eval|assert)\s*\(/.test(line))
56199
+ return "code_execution";
56200
+ if (/\b(system|exec|shell_exec|passthru|proc_open|popen)\s*\(/.test(line))
56201
+ return "command_execution";
56202
+ if (/\b(unserialize|maybe_unserialize)\s*\(/.test(line))
56203
+ return "deserialization";
56204
+ if (/\b(file_get_contents|file_put_contents|fopen|readfile|include|require|include_once|require_once)\s*\(/.test(line))
56205
+ return "file_operations";
56206
+ if (/header\s*\(\s*['"]Location/i.test(line))
56207
+ return "redirect";
56208
+ return null;
56209
+ }
56210
+ function detectWrongSanitizer(code, file) {
56211
+ const out = [];
56212
+ const ls = code.split(`
56213
+ `);
56214
+ const varSanitizer = new Map;
56215
+ for (const line of ls) {
56216
+ const m = line.match(/\$(\w+)\s*=\s*([a-zA-Z0-9_]+)\s*\(\s*([^)]*)\s*\)\s*;?/);
56217
+ if (!m)
56218
+ continue;
56219
+ const [, varName, fn, arg] = m;
56220
+ if (!SOURCE_TOKENS2.test(arg))
56221
+ continue;
56222
+ if (SANITIZERS2[fn])
56223
+ varSanitizer.set(varName, fn);
56224
+ else if (PSEUDO_SANITIZERS.has(fn))
56225
+ varSanitizer.set(varName, fn);
56226
+ }
56227
+ for (let i = 0;i < ls.length; i++) {
56228
+ const line = ls[i];
56229
+ const sinkType = sinkTypeOf(line);
56230
+ if (!sinkType)
56231
+ continue;
56232
+ const required = SINK_REQUIREMENTS[sinkType] ?? [];
56233
+ for (const [varName, fn] of varSanitizer) {
56234
+ if (!new RegExp(`\\$${varName}\\b`).test(line))
56235
+ continue;
56236
+ const sanitizer = SANITIZERS2[fn];
56237
+ const context = sanitizer?.context ?? null;
56238
+ if (sanitizer && (sanitizer.level === "validated" || context && required.includes(context)))
56239
+ continue;
56240
+ const detail = sanitizer ? `$${varName} was sanitized for '${context}' (${fn}()) but this ${sinkType} sink requires ${required.join("/")} — WRONG sanitizer (context mismatch).` : `$${varName} passed through ${fn}() — a NON-sanitizing transformation — and still reaches this ${sinkType} sink as raw tainted input. ${fn}() does not neutralize ${required.join("/")} context.`;
56241
+ out.push({
56242
+ file,
56243
+ line: i + 1,
56244
+ type: "wrong_sanitizer",
56245
+ category: "Wrong-context sanitization / pseudo-sanitizer",
56246
+ severity: "high",
56247
+ evidence: line.trim(),
56248
+ detail
56249
+ });
56250
+ }
56251
+ }
56252
+ const seen = new Set;
56253
+ return out.filter((f) => {
56254
+ const key = `${f.type}:${f.line}`;
56255
+ if (seen.has(key))
56256
+ return false;
56257
+ seen.add(key);
56258
+ return true;
56259
+ });
56260
+ }
56261
+
56262
+ // src/framework-security.ts
56263
+ var WP_PRIMITIVES = {
56264
+ framework: "wordpress",
56265
+ auth_primitives: [
56266
+ "current_user_can",
56267
+ "current_user_can_edit_post",
56268
+ "current_user_can_for_blog",
56269
+ "current_user_can_edit_others_posts",
56270
+ "is_user_logged_in",
56271
+ "authorize"
56272
+ ],
56273
+ nonce_primitives: [
56274
+ "wp_verify_nonce",
56275
+ "check_ajax_referer",
56276
+ "check_admin_referer",
56277
+ "verify_nonce"
56278
+ ],
56279
+ entry_point_hooks: [
56280
+ "wp_ajax_",
56281
+ "wp_ajax_nopriv_",
56282
+ "admin_post_",
56283
+ "admin_post_nopriv_",
56284
+ "register_rest_route",
56285
+ "add_shortcode",
56286
+ "init",
56287
+ "rest_api_init"
56288
+ ],
56289
+ sanitizers: [
56290
+ "sanitize_text_field",
56291
+ "esc_sql",
56292
+ "esc_html",
56293
+ "esc_attr",
56294
+ "esc_url",
56295
+ "wp_kses",
56296
+ "wp_kses_post",
56297
+ "absint",
56298
+ "sanitize_email",
56299
+ "sanitize_file_name"
56300
+ ]
56301
+ };
56302
+ var FRAMEWORKS = {
56303
+ wordpress: WP_PRIMITIVES,
56304
+ wp: WP_PRIMITIVES
56305
+ };
56306
+ function frameworkKnowledge(framework = "wordpress") {
56307
+ return FRAMEWORKS[framework.toLowerCase()] ?? null;
56308
+ }
56309
+ function functionBody(code, name) {
56310
+ const idx = code.search(new RegExp(`function\\s+${name}\\s*\\(`));
56311
+ if (idx === -1)
56312
+ return null;
56313
+ const open = code.indexOf("{", idx);
56314
+ if (open === -1)
56315
+ return null;
56316
+ let depth = 0;
56317
+ for (let i = open;i < code.length; i++) {
56318
+ if (code[i] === "{")
56319
+ depth++;
56320
+ else if (code[i] === "}") {
56321
+ depth--;
56322
+ if (depth === 0)
56323
+ return code.slice(open, i + 1);
56324
+ }
56325
+ }
56326
+ return code.slice(open);
56327
+ }
56328
+ function lineNoOf(code, pos) {
56329
+ let n = 1;
56330
+ for (let i = 0;i < pos; i++)
56331
+ if (code.charCodeAt(i) === 10)
56332
+ n++;
56333
+ return n;
56334
+ }
56335
+ var INDIRECT_AUTH = /\b[a-zA-Z0-9_]*(auth|permission|cap|nonce|can_|check|verify|allow)[a-zA-Z0-9_]*\s*\(/;
56336
+ function detectMissingAuthz(code, file) {
56337
+ const out = [];
56338
+ const ajaxRe = /add_action\s*\(\s*['"](wp_ajax(?:_nopriv)?_\w+)['"]\s*,\s*['"](\w+)['"]/g;
56339
+ let m;
56340
+ while ((m = ajaxRe.exec(code)) !== null) {
56341
+ const hook = m[1];
56342
+ const handler = m[2];
56343
+ const nopriv = hook.startsWith("wp_ajax_nopriv_");
56344
+ const body = functionBody(code, handler);
56345
+ if (!body)
56346
+ continue;
56347
+ const hasCap = /current_user_can|is_user_logged_in|authorize/.test(body) || INDIRECT_AUTH.test(body);
56348
+ const hasNonce = /wp_verify_nonce|check_ajax_referer|check_admin_referer|verify_nonce/.test(body);
56349
+ const line = lineNoOf(code, m.index);
56350
+ if (nopriv && !hasCap) {
56351
+ out.push({
56352
+ file,
56353
+ line,
56354
+ type: "missing_authz",
56355
+ category: "Missing capability check (unauthenticated AJAX handler)",
56356
+ severity: "high",
56357
+ evidence: `add_action('${hook}', '${handler}')`,
56358
+ detail: `Unauthenticated AJAX handler '${handler}()' performs no current_user_can()/authorization check — any unauthenticated visitor can trigger it (auth bypass / privilege escalation).`
56359
+ });
56360
+ }
56361
+ if (!hasNonce) {
56362
+ out.push({
56363
+ file,
56364
+ line,
56365
+ type: "missing_nonce",
56366
+ category: "Missing nonce verification",
56367
+ severity: "medium",
56368
+ evidence: `add_action('${hook}', '${handler}')`,
56369
+ detail: `AJAX handler '${handler}()' does not verify a nonce (wp_verify_nonce / check_ajax_referer) — CSRF: an attacker can force an authenticated admin to trigger it.`
56370
+ });
56371
+ }
56372
+ }
56373
+ const adminRe = /add_action\s*\(\s*['"](admin_post(?:_nopriv)?_\w+)['"]\s*,\s*['"](\w+)['"]/g;
56374
+ while ((m = adminRe.exec(code)) !== null) {
56375
+ const hook = m[1];
56376
+ const handler = m[2];
56377
+ const nopriv = hook.startsWith("admin_post_nopriv_");
56378
+ const body = functionBody(code, handler);
56379
+ if (!body)
56380
+ continue;
56381
+ const hasCap = /current_user_can|is_user_logged_in|authorize/.test(body) || INDIRECT_AUTH.test(body);
56382
+ const hasNonce = /wp_verify_nonce|check_admin_referer|verify_nonce/.test(body);
56383
+ const line = lineNoOf(code, m.index);
56384
+ if (nopriv && !hasCap) {
56385
+ out.push({
56386
+ file,
56387
+ line,
56388
+ type: "missing_authz",
56389
+ category: "Missing capability check (unauthenticated admin-post handler)",
56390
+ severity: "high",
56391
+ evidence: `add_action('${hook}', '${handler}')`,
56392
+ detail: `Unauthenticated admin-post handler '${handler}()' has no authorization check — auth bypass / privilege escalation.`
56393
+ });
56394
+ }
56395
+ if (!hasNonce) {
56396
+ out.push({
56397
+ file,
56398
+ line,
56399
+ type: "missing_nonce",
56400
+ category: "Missing nonce verification",
56401
+ severity: "medium",
56402
+ evidence: `add_action('${hook}', '${handler}')`,
56403
+ detail: `admin-post handler '${handler}()' does not verify a nonce — CSRF.`
56404
+ });
56405
+ }
56406
+ }
56407
+ const restRe = /register_rest_route\s*\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]/g;
56408
+ while ((m = restRe.exec(code)) !== null) {
56409
+ const line = lineNoOf(code, m.index);
56410
+ const window2 = code.slice(m.index, m.index + 800);
56411
+ if (/permission_callback/.test(window2))
56412
+ continue;
56413
+ out.push({
56414
+ file,
56415
+ line,
56416
+ type: "missing_authz",
56417
+ category: "REST route missing permission_callback",
56418
+ severity: "high",
56419
+ evidence: `register_rest_route('${m[1]}', '${m[2]}')`,
56420
+ detail: `REST route '${m[1]}${m[2]}' has no permission_callback — the endpoint defaults to public (auth bypass / unauthenticated data access or mutation).`
56421
+ });
56422
+ }
56423
+ const seen = new Set;
56424
+ return out.filter((f) => {
56425
+ const key = `${f.type}:${f.line}`;
56426
+ if (seen.has(key))
56427
+ return false;
56428
+ seen.add(key);
56429
+ return true;
56430
+ });
56431
+ }
56432
+
56081
56433
  // src/complex-bugs.ts
56082
- var SOURCE_TOKENS2 = /\$_(GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|\$request->|\$req->|request\.(get|input|query|post|data|body|headers|cookies|params)|getParameter\(|req\.(query|body|params|headers|cookies)/;
56434
+ var SOURCE_TOKENS3 = /\$_(GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|\$request->|\$req->|request\.(get|input|query|post|data|body|headers|cookies|params)|getParameter\(|req\.(query|body|params|headers|cookies)/;
56083
56435
  function lines2(code) {
56084
56436
  return code.split(`
56085
56437
  `);
@@ -56103,7 +56455,7 @@ function detectDeserialization(code, file, out) {
56103
56455
  const l = ls[ln - 1] ?? "";
56104
56456
  const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
56105
56457
  `);
56106
- if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
56458
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56107
56459
  continue;
56108
56460
  out.push({
56109
56461
  file,
@@ -56125,7 +56477,7 @@ function detectTypeJuggling(code, file, out) {
56125
56477
  const l = ls[ln - 1] ?? "";
56126
56478
  const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
56127
56479
  `);
56128
- const hasSource = SOURCE_TOKENS2.test(l) || SOURCE_TOKENS2.test(scope);
56480
+ const hasSource = SOURCE_TOKENS3.test(l) || SOURCE_TOKENS3.test(scope);
56129
56481
  const hasHash = /\b(md5|sha1|sha256|hash|crc32|crypt|password_verify|strcmp|hash_equals|secret|token|nonce)\s*\(?/i.test(l) || /\b(secret|token|nonce|hash)\b/i.test(l);
56130
56482
  if (!hasSource || !hasHash)
56131
56483
  continue;
@@ -56165,7 +56517,7 @@ function detectMassAssignment(code, file, out) {
56165
56517
  const l = ls[ln - 1] ?? "";
56166
56518
  const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
56167
56519
  `);
56168
- if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
56520
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56169
56521
  continue;
56170
56522
  out.push({
56171
56523
  file,
@@ -56189,6 +56541,8 @@ function detectComplexBugs(code, file) {
56189
56541
  detectSsrF(code, file, out);
56190
56542
  detectXxe(code, file, out);
56191
56543
  detectSsti(code, file, out);
56544
+ out.push(...detectWrongSanitizer(code, file));
56545
+ out.push(...detectMissingAuthz(code, file));
56192
56546
  const seen = new Set;
56193
56547
  return out.filter((f) => {
56194
56548
  const key = `${f.type}:${f.line}`;
@@ -56207,7 +56561,7 @@ function detectPrototypePollution(code, file, out) {
56207
56561
  const l = ls[ln - 1] ?? "";
56208
56562
  const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
56209
56563
  `);
56210
- if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
56564
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56211
56565
  continue;
56212
56566
  out.push({
56213
56567
  file,
@@ -56229,7 +56583,7 @@ function detectCrlfInjection(code, file, out) {
56229
56583
  const l = ls[ln - 1] ?? "";
56230
56584
  const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
56231
56585
  `);
56232
- if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
56586
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56233
56587
  continue;
56234
56588
  out.push({
56235
56589
  file,
@@ -56251,7 +56605,7 @@ function detectPathConfusion(code, file, out) {
56251
56605
  const l = ls[ln - 1] ?? "";
56252
56606
  const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
56253
56607
  `);
56254
- if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
56608
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56255
56609
  continue;
56256
56610
  if (/\b(basename|realpath|canonicalPath|getCanonicalPath|normalize|resolve|toRealPath)\s*\(/i.test(scope + `
56257
56611
  ` + l))
@@ -56282,7 +56636,7 @@ function detectSsrF(code, file, out) {
56282
56636
  const l = ls[ln - 1] ?? "";
56283
56637
  const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
56284
56638
  `);
56285
- if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
56639
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56286
56640
  continue;
56287
56641
  if (/(allowlist|whitelist|in_array|preg_match|parse_url|startsWith\(["']https?|endsWith\(["']|basename|realpath|normalize)/i.test(scope + `
56288
56642
  ` + l))
@@ -56310,7 +56664,7 @@ function detectXxe(code, file, out) {
56310
56664
  const l = ls[ln - 1] ?? "";
56311
56665
  const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
56312
56666
  `);
56313
- if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
56667
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56314
56668
  continue;
56315
56669
  if (/libxml_disable_entity_loader\s*\(\s*true|LIBXML_NONET/.test(scope + `
56316
56670
  ` + l))
@@ -56335,7 +56689,7 @@ function detectSsti(code, file, out) {
56335
56689
  const l = ls[ln - 1] ?? "";
56336
56690
  const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
56337
56691
  `);
56338
- if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
56692
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56339
56693
  continue;
56340
56694
  out.push({
56341
56695
  file,
@@ -57628,6 +57982,32 @@ ${statusLine}` : statusLine },
57628
57982
  }
57629
57983
  return { content: [{ type: "text", text: JSON.stringify(m) }] };
57630
57984
  });
57985
+ server.registerTool("security_state", {
57986
+ title: "Analyze a sanitizer flow (security-state lattice)",
57987
+ description: "SECURITY-STATE: given a value's sanitizer/validator functions + the sink type it reaches, return the deterministic verdict — vulnerable (wrong-context sanitization / pseudo-sanitizer / raw taint) or safe (correct context / validated). This is the step beyond binary taint: it catches 'sanitized for X but used in a Y sink'. The lattice is data-driven, no guesses.",
57988
+ inputSchema: {
57989
+ sanitizers: string2().optional().describe('JSON array of sanitizer/validator function names applied to the value (e.g. ["sanitize_text_field"])'),
57990
+ sink_type: string2().describe("Sink type: sql_execution, code_execution, command_execution, deserialization, file_operations, redirect, html_render")
57991
+ }
57992
+ }, async ({ sanitizers, sink_type }) => {
57993
+ let arr = [];
57994
+ try {
57995
+ arr = sanitizers ? JSON.parse(sanitizers) : [];
57996
+ } catch {
57997
+ return { content: [{ type: "text", text: JSON.stringify({ error: "invalid sanitizers JSON" }) }] };
57998
+ }
57999
+ return { content: [{ type: "text", text: JSON.stringify(analyzeSecurityState({ sanitizers: arr, sinkType: sink_type })) }] };
58000
+ });
58001
+ server.registerTool("framework_knowledge", {
58002
+ title: "Framework security knowledge graph",
58003
+ description: "FRAMEWORK-KNOWLEDGE: return a framework's security primitives as data — authorization primitives (current_user_can, is_user_logged_in), nonce/CSRF primitives (wp_verify_nonce, check_ajax_referer), entry-point hooks (wp_ajax_*, admin_post_*, register_rest_route), and sanitizers. Powers the missing-authz/nonce detector. Extensible to other frameworks.",
58004
+ inputSchema: {
58005
+ framework: string2().optional().describe("Framework id (default 'wordpress')")
58006
+ }
58007
+ }, async ({ framework }) => {
58008
+ const k = frameworkKnowledge(framework ?? "wordpress");
58009
+ return { content: [{ type: "text", text: JSON.stringify(k ?? { error: "framework not in the knowledge graph", framework }) }] };
58010
+ });
57631
58011
  server.registerTool("payload_lookup", {
57632
58012
  title: "Look up exploit payloads",
57633
58013
  description: "INTEL: find a payload collection (66 categories from PayloadsAllTheThings).",
@@ -58176,8 +58556,8 @@ ${statusLine}` : statusLine },
58176
58556
  };
58177
58557
  });
58178
58558
  server.registerTool("complex_scan", {
58179
- title: "Complex-bug scan (deserialization / type juggling / mass assignment / prototype pollution / CRLF / path confusion / SSRF / XXE / SSTI)",
58180
- description: "Complex-bug detection beyond taint: deserialization→POP gadget chain (unserialize + magic method), type juggling (loose ==/!= vs hash/secret), mass assignment (extract/parse_str without a safe flag), prototype pollution (unsafe merge of request data), CRLF/header injection (user input into a header), path confusion (user input into a file path without canonicalization), SSRF (user-controlled URL with no host allowlist), XXE (user XML parsed without disabling entities), and SSTI (user input into a template render with no sandbox).",
58559
+ title: "Complex-bug scan (deserialization / type juggling / mass assignment / prototype pollution / CRLF / path confusion / SSRF / XXE / SSTI / wrong-context sanitization / missing authz+nonce)",
58560
+ description: "Complex-bug detection beyond taint: deserialization→POP gadget chain (unserialize + magic method), type juggling (loose ==/!= vs hash/secret), mass assignment (extract/parse_str without a safe flag), prototype pollution (unsafe merge of request data), CRLF/header injection (user input into a header), path confusion (user input into a file path without canonicalization), SSRF (user-controlled URL with no host allowlist), XXE (user XML parsed without disabling entities), SSTI (user input into a template render with no sandbox), wrong-context sanitization (sanitize_text_field() into a SQL query), and missing authorization/nonce on WordPress entry points (wp_ajax_* / admin_post_* / register_rest_route without current_user_can, nonce verification, or permission_callback).",
58181
58561
  inputSchema: { path: string2().describe("Source file path") }
58182
58562
  }, async ({ path }) => {
58183
58563
  const code = readFileSync21(path, "utf8");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blitzstrike",
3
- "version": "2.4.32",
3
+ "version": "2.4.34",
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": {