blitzstrike 2.4.32 → 2.4.33

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.33] — 2026-09-14
9
+
10
+ ### Added — Security-State Lattice (beyond binary taint)
11
+
12
+ - New `src/security-state.ts`: models each value's SECURITY STATE
13
+ (clean < whitelisted < validated < sanitized(context) < tainted) instead of
14
+ binary tainted/not. A sink declares the minimum context it accepts.
15
+ - `detectWrongSanitizer` (wired into `complex_scan`/`blitz_file`) finds the
16
+ complex bug class binary taint misses: a value sanitized for context X
17
+ reaching a sink that needs Y (e.g. `sanitize_text_field()` into a SQL query),
18
+ and pseudo-sanitizers (`base64_encode()`/`trim()` misused as escaping).
19
+ - New `security_state` tool: deterministic verdict for a sanitizer flow.
20
+ - Data-driven tables (sanitizers, pseudo-sanitizers, sink requirements) —
21
+ the lattice is data, not guesses.
22
+
8
23
  ## [2.4.32] — 2026-09-14
9
24
 
10
25
  ### Removed
@@ -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,189 @@ 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
+
56081
56262
  // 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)/;
56263
+ 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
56264
  function lines2(code) {
56084
56265
  return code.split(`
56085
56266
  `);
@@ -56103,7 +56284,7 @@ function detectDeserialization(code, file, out) {
56103
56284
  const l = ls[ln - 1] ?? "";
56104
56285
  const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
56105
56286
  `);
56106
- if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
56287
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56107
56288
  continue;
56108
56289
  out.push({
56109
56290
  file,
@@ -56125,7 +56306,7 @@ function detectTypeJuggling(code, file, out) {
56125
56306
  const l = ls[ln - 1] ?? "";
56126
56307
  const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
56127
56308
  `);
56128
- const hasSource = SOURCE_TOKENS2.test(l) || SOURCE_TOKENS2.test(scope);
56309
+ const hasSource = SOURCE_TOKENS3.test(l) || SOURCE_TOKENS3.test(scope);
56129
56310
  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
56311
  if (!hasSource || !hasHash)
56131
56312
  continue;
@@ -56165,7 +56346,7 @@ function detectMassAssignment(code, file, out) {
56165
56346
  const l = ls[ln - 1] ?? "";
56166
56347
  const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
56167
56348
  `);
56168
- if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
56349
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56169
56350
  continue;
56170
56351
  out.push({
56171
56352
  file,
@@ -56189,6 +56370,7 @@ function detectComplexBugs(code, file) {
56189
56370
  detectSsrF(code, file, out);
56190
56371
  detectXxe(code, file, out);
56191
56372
  detectSsti(code, file, out);
56373
+ out.push(...detectWrongSanitizer(code, file));
56192
56374
  const seen = new Set;
56193
56375
  return out.filter((f) => {
56194
56376
  const key = `${f.type}:${f.line}`;
@@ -56207,7 +56389,7 @@ function detectPrototypePollution(code, file, out) {
56207
56389
  const l = ls[ln - 1] ?? "";
56208
56390
  const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
56209
56391
  `);
56210
- if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
56392
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56211
56393
  continue;
56212
56394
  out.push({
56213
56395
  file,
@@ -56229,7 +56411,7 @@ function detectCrlfInjection(code, file, out) {
56229
56411
  const l = ls[ln - 1] ?? "";
56230
56412
  const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
56231
56413
  `);
56232
- if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
56414
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56233
56415
  continue;
56234
56416
  out.push({
56235
56417
  file,
@@ -56251,7 +56433,7 @@ function detectPathConfusion(code, file, out) {
56251
56433
  const l = ls[ln - 1] ?? "";
56252
56434
  const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
56253
56435
  `);
56254
- if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
56436
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56255
56437
  continue;
56256
56438
  if (/\b(basename|realpath|canonicalPath|getCanonicalPath|normalize|resolve|toRealPath)\s*\(/i.test(scope + `
56257
56439
  ` + l))
@@ -56282,7 +56464,7 @@ function detectSsrF(code, file, out) {
56282
56464
  const l = ls[ln - 1] ?? "";
56283
56465
  const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
56284
56466
  `);
56285
- if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
56467
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56286
56468
  continue;
56287
56469
  if (/(allowlist|whitelist|in_array|preg_match|parse_url|startsWith\(["']https?|endsWith\(["']|basename|realpath|normalize)/i.test(scope + `
56288
56470
  ` + l))
@@ -56310,7 +56492,7 @@ function detectXxe(code, file, out) {
56310
56492
  const l = ls[ln - 1] ?? "";
56311
56493
  const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
56312
56494
  `);
56313
- if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
56495
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56314
56496
  continue;
56315
56497
  if (/libxml_disable_entity_loader\s*\(\s*true|LIBXML_NONET/.test(scope + `
56316
56498
  ` + l))
@@ -56335,7 +56517,7 @@ function detectSsti(code, file, out) {
56335
56517
  const l = ls[ln - 1] ?? "";
56336
56518
  const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
56337
56519
  `);
56338
- if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
56520
+ if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
56339
56521
  continue;
56340
56522
  out.push({
56341
56523
  file,
@@ -57628,6 +57810,22 @@ ${statusLine}` : statusLine },
57628
57810
  }
57629
57811
  return { content: [{ type: "text", text: JSON.stringify(m) }] };
57630
57812
  });
57813
+ server.registerTool("security_state", {
57814
+ title: "Analyze a sanitizer flow (security-state lattice)",
57815
+ 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.",
57816
+ inputSchema: {
57817
+ sanitizers: string2().optional().describe('JSON array of sanitizer/validator function names applied to the value (e.g. ["sanitize_text_field"])'),
57818
+ sink_type: string2().describe("Sink type: sql_execution, code_execution, command_execution, deserialization, file_operations, redirect, html_render")
57819
+ }
57820
+ }, async ({ sanitizers, sink_type }) => {
57821
+ let arr = [];
57822
+ try {
57823
+ arr = sanitizers ? JSON.parse(sanitizers) : [];
57824
+ } catch {
57825
+ return { content: [{ type: "text", text: JSON.stringify({ error: "invalid sanitizers JSON" }) }] };
57826
+ }
57827
+ return { content: [{ type: "text", text: JSON.stringify(analyzeSecurityState({ sanitizers: arr, sinkType: sink_type })) }] };
57828
+ });
57631
57829
  server.registerTool("payload_lookup", {
57632
57830
  title: "Look up exploit payloads",
57633
57831
  description: "INTEL: find a payload collection (66 categories from PayloadsAllTheThings).",
@@ -58176,8 +58374,8 @@ ${statusLine}` : statusLine },
58176
58374
  };
58177
58375
  });
58178
58376
  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).",
58377
+ title: "Complex-bug scan (deserialization / type juggling / mass assignment / prototype pollution / CRLF / path confusion / SSRF / XXE / SSTI / wrong-context sanitization)",
58378
+ 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), and wrong-context sanitization (a value sanitized for context X reaching a sink that needs Y — e.g. sanitize_text_field() into a SQL query).",
58181
58379
  inputSchema: { path: string2().describe("Source file path") }
58182
58380
  }, async ({ path }) => {
58183
58381
  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.33",
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": {