blitzstrike 1.0.59 → 1.0.60
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 +28 -0
- package/bench/corpus/wp-author-not-in-fixed.md +21 -0
- package/bench/corpus/wp-author-not-in-vuln.md +23 -0
- package/dist/index.js +51 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ 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.60] — 2026-09-13
|
|
9
|
+
|
|
10
|
+
### Added — deep taint-flow + CVE pattern coverage (CVE-2026-60137)
|
|
11
|
+
|
|
12
|
+
Investigated the WordPress core chain CVE-2026-60137 (`author__not_in` SQLi) /
|
|
13
|
+
CVE-2026-63030 (REST API batch route confusion → RCE). The SQLi base was a
|
|
14
|
+
**type-confusion sanitizer bypass**: `absint` guarded by `if ( is_array() )` +
|
|
15
|
+
a `(array)` cast + `implode` that re-interpolates the raw string. The engine
|
|
16
|
+
now detects it:
|
|
17
|
+
|
|
18
|
+
- **Method-call resolution** (`calleeName`) — `$obj->method()` now resolves to
|
|
19
|
+
its method definition for interprocedural taint, not the object variable.
|
|
20
|
+
- **Built-in taint propagation** — `implode`/`join`/`sprintf`/`str_replace` and
|
|
21
|
+
other array→string/format built-ins now carry taint through to the sink.
|
|
22
|
+
- **Cast handling** — `(array)`/`(string)` casts propagate taint (a string can
|
|
23
|
+
still carry a payload); `(int)`/`(float)` casts act as an int_cast sanitizer.
|
|
24
|
+
- **`wp_parse_id_list`** added as an integer-cast sanitizer.
|
|
25
|
+
|
|
26
|
+
The conditional-flow merge already models the `is_array` guard correctly, so the
|
|
27
|
+
engine distinguishes the bypassable sanitizer (vulnerable) from the unconditional
|
|
28
|
+
one (fixed). Added two corpus fixtures (`wp-author-not-in-vuln`/`-fixed`).
|
|
29
|
+
|
|
30
|
+
### Not yet detected
|
|
31
|
+
|
|
32
|
+
- **CVE-2026-63030** (REST API batch endpoint route confusion) is a routing /
|
|
33
|
+
authorization-logic bug, not a source→sink taint flow — out of scope for the
|
|
34
|
+
current taint engine.
|
|
35
|
+
|
|
8
36
|
## [1.0.59] — 2026-09-13
|
|
9
37
|
|
|
10
38
|
### Added — enterprise-grade detection + benchmark
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
- id: wp-author-not-in-fixed
|
|
2
|
+
- language: php
|
|
3
|
+
- vulnerable: false
|
|
4
|
+
- sink_type: sql_execution
|
|
5
|
+
```php
|
|
6
|
+
<?php
|
|
7
|
+
class WP_Query {
|
|
8
|
+
public function get_posts($qv) {
|
|
9
|
+
$where = "";
|
|
10
|
+
if ( ! empty( $qv["author__not_in"] ) ) {
|
|
11
|
+
$ids = wp_parse_id_list( $qv["author__not_in"] );
|
|
12
|
+
$author__not_in = implode( ",", $ids );
|
|
13
|
+
$where .= " AND post_author NOT IN ($author__not_in) ";
|
|
14
|
+
}
|
|
15
|
+
return $wpdb->query("SELECT * FROM posts WHERE 1=1 $where");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
$q = $_GET;
|
|
19
|
+
$w = new WP_Query();
|
|
20
|
+
$w->get_posts($q);
|
|
21
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
- id: wp-author-not-in-vuln
|
|
2
|
+
- language: php
|
|
3
|
+
- vulnerable: true
|
|
4
|
+
- sink_type: sql_execution
|
|
5
|
+
```php
|
|
6
|
+
<?php
|
|
7
|
+
class WP_Query {
|
|
8
|
+
public function get_posts($qv) {
|
|
9
|
+
$where = "";
|
|
10
|
+
if ( ! empty( $qv["author__not_in"] ) ) {
|
|
11
|
+
if ( is_array( $qv["author__not_in"] ) ) {
|
|
12
|
+
$qv["author__not_in"] = array_map( "absint", $qv["author__not_in"] );
|
|
13
|
+
}
|
|
14
|
+
$author__not_in = implode( ",", (array) $qv["author__not_in"] );
|
|
15
|
+
$where .= " AND post_author NOT IN ($author__not_in) ";
|
|
16
|
+
}
|
|
17
|
+
return $wpdb->query("SELECT * FROM posts WHERE 1=1 $where");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
$q = $_GET;
|
|
21
|
+
$w = new WP_Query();
|
|
22
|
+
$w->get_posts($q);
|
|
23
|
+
```
|
package/dist/index.js
CHANGED
|
@@ -9190,6 +9190,7 @@ var init_dataflow = __esm(() => {
|
|
|
9190
9190
|
escapeshellcmd: { id: "shell_escape", label: "Shell command escape", neutralizes: ["command_execution"] },
|
|
9191
9191
|
intval: { id: "int_cast", label: "Integer cast", neutralizes: ["sql_execution", "command_execution", "file_inclusion"] },
|
|
9192
9192
|
absint: { id: "int_cast", label: "Absolute integer cast", neutralizes: ["sql_execution", "command_execution", "file_inclusion"] },
|
|
9193
|
+
wp_parse_id_list: { id: "int_cast", label: "WordPress ID list cast", neutralizes: ["sql_execution", "command_execution", "file_inclusion"] },
|
|
9193
9194
|
"preg_replace.*FILTER": { id: "regex_filter", label: "Regex filter", neutralizes: ["html_render"] }
|
|
9194
9195
|
};
|
|
9195
9196
|
});
|
|
@@ -17104,6 +17105,15 @@ function rootVarName2(node) {
|
|
|
17104
17105
|
return rootVarName2(node.what);
|
|
17105
17106
|
return null;
|
|
17106
17107
|
}
|
|
17108
|
+
function calleeName(what) {
|
|
17109
|
+
if (!isNode3(what))
|
|
17110
|
+
return null;
|
|
17111
|
+
if (what.kind === "name")
|
|
17112
|
+
return typeof what.name === "string" ? what.name : what.name?.name ?? null;
|
|
17113
|
+
if (what.kind === "propertylookup" || what.kind === "staticlookup")
|
|
17114
|
+
return what.offset?.name ?? what.offset?.value ?? null;
|
|
17115
|
+
return null;
|
|
17116
|
+
}
|
|
17107
17117
|
function sourceOf2(node) {
|
|
17108
17118
|
if (!isNode3(node))
|
|
17109
17119
|
return null;
|
|
@@ -17268,6 +17278,14 @@ function analyzeExpr(node, env, summary, ctx, taintedParam, props, activeAuth, s
|
|
|
17268
17278
|
const src = sourceOf2(node);
|
|
17269
17279
|
if (src)
|
|
17270
17280
|
return { tainted: true, sources: [src], sanitizers: [], sourceLine: node.loc?.start?.line };
|
|
17281
|
+
if (node.kind === "cast") {
|
|
17282
|
+
const inner = analyzeExpr(node.expr, env, summary, ctx, taintedParam, props, activeAuth, summaries, validating);
|
|
17283
|
+
const castType = String(node.type ?? "");
|
|
17284
|
+
if (/^(int|integer|bool|boolean|float|double)$/i.test(castType) && inner.tainted) {
|
|
17285
|
+
return { ...inner, sanitizers: [...inner.sanitizers, { id: "int_cast", label: "Numeric cast", neutralizes: ["sql_execution", "command_execution", "file_inclusion"] }] };
|
|
17286
|
+
}
|
|
17287
|
+
return inner;
|
|
17288
|
+
}
|
|
17271
17289
|
if (node.kind === "assign") {
|
|
17272
17290
|
const byref = node.operator === "=&";
|
|
17273
17291
|
const rhs = analyzeExpr(node.right, env, summary, ctx, taintedParam, props, activeAuth, summaries, validating);
|
|
@@ -17315,8 +17333,11 @@ function analyzeExpr(node, env, summary, ctx, taintedParam, props, activeAuth, s
|
|
|
17315
17333
|
analyzeExpr(a, env, summary, ctx, taintedParam, props, activeAuth, summaries, validating);
|
|
17316
17334
|
return taintedArgs.length > 0 ? mergeTaint(...taintedArgs) : CLEAN;
|
|
17317
17335
|
}
|
|
17318
|
-
const fnName =
|
|
17336
|
+
const fnName = calleeName(node.what);
|
|
17319
17337
|
if (fnName) {
|
|
17338
|
+
if (TAINT_PROPAGATING_BUILTINS.has(fnName) && taintedArgs.length > 0) {
|
|
17339
|
+
return mergeTaint(...taintedArgs);
|
|
17340
|
+
}
|
|
17320
17341
|
const san = findSanitizers(fnName + "(");
|
|
17321
17342
|
if (san.length > 0) {
|
|
17322
17343
|
if (taintedArgs.length > 0) {
|
|
@@ -17575,7 +17596,7 @@ function analyzeDataFlow2(path) {
|
|
|
17575
17596
|
const seen = new Set;
|
|
17576
17597
|
const mainCalls = topStmts.flatMap((s) => findKind2(s, "call"));
|
|
17577
17598
|
for (const call of mainCalls) {
|
|
17578
|
-
const fnName =
|
|
17599
|
+
const fnName = calleeName(call.what);
|
|
17579
17600
|
if (!fnName || sinkClassOf2(call))
|
|
17580
17601
|
continue;
|
|
17581
17602
|
const target = resolveFuncId(fnName, summaries);
|
|
@@ -17662,7 +17683,7 @@ function analyzeDataFlow2(path) {
|
|
|
17662
17683
|
setCache("eagle2", cacheCode, result);
|
|
17663
17684
|
return result;
|
|
17664
17685
|
}
|
|
17665
|
-
var import_php_parser3, parser3, CLEAN, VALIDATION_GATES;
|
|
17686
|
+
var import_php_parser3, parser3, TAINT_PROPAGATING_BUILTINS, CLEAN, VALIDATION_GATES;
|
|
17666
17687
|
var init_eagle2 = __esm(() => {
|
|
17667
17688
|
init_dataflow();
|
|
17668
17689
|
init_scanner();
|
|
@@ -17673,6 +17694,33 @@ var init_eagle2 = __esm(() => {
|
|
|
17673
17694
|
parser: { extractDoc: false, php7: true },
|
|
17674
17695
|
ast: { withPositions: true, withSource: false }
|
|
17675
17696
|
});
|
|
17697
|
+
TAINT_PROPAGATING_BUILTINS = new Set([
|
|
17698
|
+
"implode",
|
|
17699
|
+
"join",
|
|
17700
|
+
"sprintf",
|
|
17701
|
+
"vsprintf",
|
|
17702
|
+
"str_replace",
|
|
17703
|
+
"str_ireplace",
|
|
17704
|
+
"substr",
|
|
17705
|
+
"mb_substr",
|
|
17706
|
+
"substr_replace",
|
|
17707
|
+
"trim",
|
|
17708
|
+
"ltrim",
|
|
17709
|
+
"rtrim",
|
|
17710
|
+
"strtolower",
|
|
17711
|
+
"strtoupper",
|
|
17712
|
+
"ucfirst",
|
|
17713
|
+
"lcfirst",
|
|
17714
|
+
"urldecode",
|
|
17715
|
+
"rawurldecode",
|
|
17716
|
+
"base64_decode",
|
|
17717
|
+
"html_entity_decode",
|
|
17718
|
+
"stripslashes",
|
|
17719
|
+
"addslashes",
|
|
17720
|
+
"strval",
|
|
17721
|
+
"preg_quote",
|
|
17722
|
+
"nl2br"
|
|
17723
|
+
]);
|
|
17676
17724
|
CLEAN = { tainted: false, sources: [], sanitizers: [] };
|
|
17677
17725
|
VALIDATION_GATES = ["in_array", "preg_match", "ctype_digit", "ctype_alnum", "is_numeric"];
|
|
17678
17726
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blitzstrike",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.60",
|
|
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": {
|