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
|
+
```
|
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
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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),
|
|
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.
|
|
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": {
|