blitzstrike 1.0.60 → 1.0.63
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 +41 -18
- package/bench/corpus/php-int-cast-bypass-fixed.md +20 -0
- package/bench/corpus/php-int-cast-bypass-vuln.md +23 -0
- package/bench/enterprise/php-complex/plugin.php +34 -0
- package/bench/enterprise/wp-batch-route/batch.php +43 -0
- package/dist/index.js +317 -1
- package/package.json +1 -1
- package/bench/corpus/wp-author-not-in-fixed.md +0 -21
- package/bench/corpus/wp-author-not-in-vuln.md +0 -23
package/CHANGELOG.md
CHANGED
|
@@ -5,15 +5,47 @@ 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.
|
|
8
|
+
## [1.0.63] — 2026-09-13
|
|
9
|
+
|
|
10
|
+
### Added — complex-bug detection (`src/complex-bugs.ts`)
|
|
11
|
+
|
|
12
|
+
Three "hard" vulnerability classes beyond source→sink taint:
|
|
13
|
+
|
|
14
|
+
- **Deserialization → POP gadget chain** — `unserialize()` of attacker-controlled
|
|
15
|
+
data when a magic method (`__destruct`/`__wakeup`/`__toString`/`__invoke`/`__call`)
|
|
16
|
+
is present in the file (object injection → RCE pivot).
|
|
17
|
+
- **Type juggling** — loose comparison (`==`/`!=`) of user input against a
|
|
18
|
+
hash/secret (PHP `0e…` collision auth bypass); strict `===` is ignored.
|
|
19
|
+
- **Mass assignment** — `extract()` of request data without `EXTR_SKIP`, or
|
|
20
|
+
`parse_str()` of user input (variable injection).
|
|
21
|
+
|
|
22
|
+
Wired into the `@source` agent as `vulnerability` graph nodes. Added a
|
|
23
|
+
`php-complex` fixture to the enterprise benchmark.
|
|
24
|
+
|
|
25
|
+
## [1.0.62] — 2026-09-13
|
|
26
|
+
|
|
27
|
+
### Added — route-confusion & dispatch-abuse detection
|
|
9
28
|
|
|
10
|
-
|
|
29
|
+
New vulnerability class distinct from source→sink taint (`src/route-confusion.ts`):
|
|
30
|
+
detects when the routing/dispatch layer lets an attacker reach an unintended
|
|
31
|
+
handler or control which handler runs.
|
|
11
32
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
33
|
+
- `dynamic_dispatch` — attacker-controlled callback/route (`call_user_func`,
|
|
34
|
+
`forward`, `dispatch`, `resolve_route`, `invoke`).
|
|
35
|
+
- `dynamic_method_call` — `$obj->$method()` / `$class::$method()`.
|
|
36
|
+
- `dynamic_include` — `include`/`require` of a variable (LFI via route).
|
|
37
|
+
- `batch_forwarding` — a loop over sub-requests that forwards to inner handlers
|
|
38
|
+
without a per-route authorization re-check.
|
|
39
|
+
- `route_normalization_gap` — route matching against user input without path
|
|
40
|
+
normalization (strtolower/rtrim/urldecode).
|
|
41
|
+
|
|
42
|
+
Wired into the `@source` agent; findings become `route` nodes in the security
|
|
43
|
+
graph. Added a WordPress-style batch-endpoint fixture to the enterprise
|
|
44
|
+
benchmark (`wp-batch-route`).
|
|
45
|
+
|
|
46
|
+
## [1.0.60] — 2026-09-13
|
|
47
|
+
|
|
48
|
+
### Added — deeper taint-flow coverage
|
|
17
49
|
|
|
18
50
|
- **Method-call resolution** (`calleeName`) — `$obj->method()` now resolves to
|
|
19
51
|
its method definition for interprocedural taint, not the object variable.
|
|
@@ -21,17 +53,8 @@ now detects it:
|
|
|
21
53
|
other array→string/format built-ins now carry taint through to the sink.
|
|
22
54
|
- **Cast handling** — `(array)`/`(string)` casts propagate taint (a string can
|
|
23
55
|
still carry a payload); `(int)`/`(float)` casts act as an int_cast sanitizer.
|
|
24
|
-
-
|
|
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.
|
|
56
|
+
- New corpus fixtures covering conditional-sanitizer bypass on array→SQL
|
|
57
|
+
interpolation (`php-int-cast-bypass-vuln`/`-fixed`).
|
|
35
58
|
|
|
36
59
|
## [1.0.59] — 2026-09-13
|
|
37
60
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
- id: php-int-cast-bypass-fixed
|
|
2
|
+
- language: php
|
|
3
|
+
- vulnerable: false
|
|
4
|
+
- sink_type: sql_execution
|
|
5
|
+
```php
|
|
6
|
+
<?php
|
|
7
|
+
class Query {
|
|
8
|
+
public function run($params) {
|
|
9
|
+
$where = "";
|
|
10
|
+
if ( ! empty( $params["ids"] ) ) {
|
|
11
|
+
$ids = implode( ",", array_map( "intval", (array) $params["ids"] ) );
|
|
12
|
+
$where .= " AND id NOT IN ($ids) ";
|
|
13
|
+
}
|
|
14
|
+
return $db->query("SELECT * FROM t WHERE 1=1 $where");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
$q = $_GET;
|
|
18
|
+
$w = new Query();
|
|
19
|
+
$w->run($q);
|
|
20
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
- id: php-int-cast-bypass-vuln
|
|
2
|
+
- language: php
|
|
3
|
+
- vulnerable: true
|
|
4
|
+
- sink_type: sql_execution
|
|
5
|
+
```php
|
|
6
|
+
<?php
|
|
7
|
+
class Query {
|
|
8
|
+
public function run($params) {
|
|
9
|
+
$where = "";
|
|
10
|
+
if ( ! empty( $params["ids"] ) ) {
|
|
11
|
+
if ( is_array( $params["ids"] ) ) {
|
|
12
|
+
$params["ids"] = array_map( "intval", $params["ids"] );
|
|
13
|
+
}
|
|
14
|
+
$ids = implode( ",", (array) $params["ids"] );
|
|
15
|
+
$where .= " AND id NOT IN ($ids) ";
|
|
16
|
+
}
|
|
17
|
+
return $db->query("SELECT * FROM t WHERE 1=1 $where");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
$q = $_GET;
|
|
21
|
+
$w = new Query();
|
|
22
|
+
$w->run($q);
|
|
23
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
/**
|
|
3
|
+
* Plugin helper (simplified) with multiple complex-bug patterns.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// 1. Deserialization → object injection (magic method present)
|
|
7
|
+
function restore_session($payload) {
|
|
8
|
+
$data = $_POST['data'];
|
|
9
|
+
$obj = unserialize($data);
|
|
10
|
+
return $obj;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class CacheManager {
|
|
14
|
+
public function __destruct() {
|
|
15
|
+
// Magic method — a POP gadget can pivot object injection to RCE here.
|
|
16
|
+
system("rm -rf /tmp/cache");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 2. Type juggling — loose comparison of user input against a hash
|
|
21
|
+
function verify_signature() {
|
|
22
|
+
$sig = $_GET['sig'];
|
|
23
|
+
$secret = get_option('signing_secret');
|
|
24
|
+
if ($sig == $secret) { // <-- loose == , 0e... collision bypass
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 3. Mass assignment — extract() of request data without EXTR_SKIP
|
|
31
|
+
function bootstrap_options() {
|
|
32
|
+
extract($_REQUEST); // <-- variable injection
|
|
33
|
+
return $admin; // $admin is attacker-controlled
|
|
34
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
/**
|
|
3
|
+
* REST API batch endpoint (simplified) — forwards to inner handlers.
|
|
4
|
+
*
|
|
5
|
+
* The `/batch` route is registered WITHOUT a permission_callback, so it is
|
|
6
|
+
* reachable unauthenticated. A malformed nested request desynchronizes the
|
|
7
|
+
* batch router, letting an attacker reach handlers they should not.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
add_action('rest_api_init', function () {
|
|
11
|
+
register_rest_route('my/v1', '/batch', array(
|
|
12
|
+
'methods' => 'POST',
|
|
13
|
+
'callback' => 'handle_batch',
|
|
14
|
+
'permission_callback' => '__return_true', // <-- no real authz check
|
|
15
|
+
));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function handle_batch($request) {
|
|
19
|
+
$operations = $request['requests'];
|
|
20
|
+
$responses = array();
|
|
21
|
+
|
|
22
|
+
foreach ($operations as $op) {
|
|
23
|
+
$route = $op['route'];
|
|
24
|
+
$handler = resolve_route($route);
|
|
25
|
+
$responses[] = $handler($op['params']);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return $responses;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolve_route($route) {
|
|
32
|
+
$registry = array(
|
|
33
|
+
'users' => 'get_users',
|
|
34
|
+
'posts' => 'get_posts',
|
|
35
|
+
'options' => 'get_options',
|
|
36
|
+
);
|
|
37
|
+
return $registry[$route];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function get_users($params) {
|
|
41
|
+
global $wpdb;
|
|
42
|
+
return $wpdb->get_results("SELECT * FROM users WHERE id = " . $params['id']);
|
|
43
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -9190,7 +9190,6 @@ 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"] },
|
|
9194
9193
|
"preg_replace.*FILTER": { id: "regex_filter", label: "Regex filter", neutralizes: ["html_render"] }
|
|
9195
9194
|
};
|
|
9196
9195
|
});
|
|
@@ -37050,6 +37049,269 @@ function reviewValidated(mission) {
|
|
|
37050
37049
|
}
|
|
37051
37050
|
var init_reviewer = () => {};
|
|
37052
37051
|
|
|
37052
|
+
// src/route-confusion.ts
|
|
37053
|
+
function lines(code) {
|
|
37054
|
+
return code.split(`
|
|
37055
|
+
`);
|
|
37056
|
+
}
|
|
37057
|
+
function lineNo(code, pos) {
|
|
37058
|
+
let n = 1;
|
|
37059
|
+
for (let i = 0;i < pos; i++)
|
|
37060
|
+
if (code.charCodeAt(i) === 10)
|
|
37061
|
+
n++;
|
|
37062
|
+
return n;
|
|
37063
|
+
}
|
|
37064
|
+
function userControlled(scope) {
|
|
37065
|
+
return SOURCE_TOKENS.test(scope);
|
|
37066
|
+
}
|
|
37067
|
+
function detectDynamicDispatch(code, file, out) {
|
|
37068
|
+
const ls = lines(code);
|
|
37069
|
+
DYNAMIC_DISPATCH_RE.lastIndex = 0;
|
|
37070
|
+
let m;
|
|
37071
|
+
while ((m = DYNAMIC_DISPATCH_RE.exec(code)) !== null) {
|
|
37072
|
+
const ln = lineNo(code, m.index);
|
|
37073
|
+
const l = ls[ln - 1] ?? "";
|
|
37074
|
+
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
37075
|
+
`);
|
|
37076
|
+
if (!userControlled(l) && !userControlled(scope))
|
|
37077
|
+
continue;
|
|
37078
|
+
out.push({
|
|
37079
|
+
file,
|
|
37080
|
+
line: ln,
|
|
37081
|
+
type: "dynamic_dispatch",
|
|
37082
|
+
category: "Route confusion (user-controlled dispatch)",
|
|
37083
|
+
severity: "high",
|
|
37084
|
+
evidence: l.trim(),
|
|
37085
|
+
detail: `\`${m[1]}()\` invoked with an attacker-controlled callback/route — the request selects the handler.`
|
|
37086
|
+
});
|
|
37087
|
+
}
|
|
37088
|
+
}
|
|
37089
|
+
function detectDynamicMethod(code, file, out) {
|
|
37090
|
+
const ls = lines(code);
|
|
37091
|
+
DYNAMIC_METHOD_RE.lastIndex = 0;
|
|
37092
|
+
let m;
|
|
37093
|
+
while ((m = DYNAMIC_METHOD_RE.exec(code)) !== null) {
|
|
37094
|
+
const ln = lineNo(code, m.index);
|
|
37095
|
+
const l = ls[ln - 1] ?? "";
|
|
37096
|
+
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
37097
|
+
`);
|
|
37098
|
+
if (!userControlled(l) && !userControlled(scope))
|
|
37099
|
+
continue;
|
|
37100
|
+
out.push({
|
|
37101
|
+
file,
|
|
37102
|
+
line: ln,
|
|
37103
|
+
type: "dynamic_method_call",
|
|
37104
|
+
category: "Route confusion (dynamic method dispatch)",
|
|
37105
|
+
severity: "high",
|
|
37106
|
+
evidence: l.trim(),
|
|
37107
|
+
detail: "Dynamic method/static call `" + m[1] + "$var()` — the invoked symbol is attacker-influenced."
|
|
37108
|
+
});
|
|
37109
|
+
}
|
|
37110
|
+
}
|
|
37111
|
+
function detectDynamicInclude(code, file, out) {
|
|
37112
|
+
const ls = lines(code);
|
|
37113
|
+
const re = /\b(include|require)(_once)?\s*\(\s*(\$[A-Za-z_]\w*)/g;
|
|
37114
|
+
let m;
|
|
37115
|
+
while ((m = re.exec(code)) !== null) {
|
|
37116
|
+
const ln = lineNo(code, m.index);
|
|
37117
|
+
const l = ls[ln - 1] ?? "";
|
|
37118
|
+
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
37119
|
+
`);
|
|
37120
|
+
if (!userControlled(l) && !userControlled(scope))
|
|
37121
|
+
continue;
|
|
37122
|
+
out.push({
|
|
37123
|
+
file,
|
|
37124
|
+
line: ln,
|
|
37125
|
+
type: "dynamic_include",
|
|
37126
|
+
category: "Route confusion (dynamic file include)",
|
|
37127
|
+
severity: "high",
|
|
37128
|
+
evidence: l.trim(),
|
|
37129
|
+
detail: `\`${m[1]}(…)\` of a variable — potential LFI/RCE when the path is attacker-controlled.`
|
|
37130
|
+
});
|
|
37131
|
+
}
|
|
37132
|
+
}
|
|
37133
|
+
function detectBatchForwarding(code, file, out) {
|
|
37134
|
+
BATCH_LOOP_RE.lastIndex = 0;
|
|
37135
|
+
let m;
|
|
37136
|
+
while ((m = BATCH_LOOP_RE.exec(code)) !== null) {
|
|
37137
|
+
const ln = lineNo(code, m.index);
|
|
37138
|
+
const body = lines(code).slice(ln, ln + 8).join(`
|
|
37139
|
+
`);
|
|
37140
|
+
const forwards = /\b(call_user_func|forward|dispatch|resolve_route|invoke|handle|route)\s*\(/i.test(body);
|
|
37141
|
+
if (!forwards)
|
|
37142
|
+
continue;
|
|
37143
|
+
const rechecks = /\b(current_user_can|permission_callback|check_ajax_referer|authorize|authorise|is_user_logged_in)\b/i.test(body);
|
|
37144
|
+
out.push({
|
|
37145
|
+
file,
|
|
37146
|
+
line: ln,
|
|
37147
|
+
type: "batch_forwarding",
|
|
37148
|
+
category: "Route confusion (batch/proxy forwarding)",
|
|
37149
|
+
severity: rechecks ? "medium" : "high",
|
|
37150
|
+
evidence: lines(code)[ln - 1]?.trim() ?? "",
|
|
37151
|
+
detail: rechecks ? "Batch/proxy loop forwards to inner handlers — auth is re-checked in scope, but verify it applies per-route." : "Batch/proxy loop forwards to inner handlers with no per-route authorization re-check — an attacker may reach a protected handler."
|
|
37152
|
+
});
|
|
37153
|
+
}
|
|
37154
|
+
}
|
|
37155
|
+
function detectRouteNormalization(code, file, out) {
|
|
37156
|
+
const ls = lines(code);
|
|
37157
|
+
for (let i = 0;i < ls.length; i++) {
|
|
37158
|
+
const l = ls[i];
|
|
37159
|
+
if (!ROUTE_MATCH_RE.test(l))
|
|
37160
|
+
continue;
|
|
37161
|
+
const scope = ls.slice(Math.max(0, i - 3), i + 1).join(`
|
|
37162
|
+
`);
|
|
37163
|
+
if (!userControlled(scope))
|
|
37164
|
+
continue;
|
|
37165
|
+
if (NORMALIZERS.test(l) || NORMALIZERS.test(scope))
|
|
37166
|
+
continue;
|
|
37167
|
+
out.push({
|
|
37168
|
+
file,
|
|
37169
|
+
line: i + 1,
|
|
37170
|
+
type: "route_normalization_gap",
|
|
37171
|
+
category: "Route confusion (missing path normalization)",
|
|
37172
|
+
severity: "medium",
|
|
37173
|
+
evidence: l.trim(),
|
|
37174
|
+
detail: "Route matched against user input without visible normalization (strtolower/rtrim/urldecode) — route desync possible."
|
|
37175
|
+
});
|
|
37176
|
+
}
|
|
37177
|
+
}
|
|
37178
|
+
function detectRouteConfusion(code, file) {
|
|
37179
|
+
const out = [];
|
|
37180
|
+
detectDynamicDispatch(code, file, out);
|
|
37181
|
+
detectDynamicMethod(code, file, out);
|
|
37182
|
+
detectDynamicInclude(code, file, out);
|
|
37183
|
+
detectBatchForwarding(code, file, out);
|
|
37184
|
+
detectRouteNormalization(code, file, out);
|
|
37185
|
+
return out;
|
|
37186
|
+
}
|
|
37187
|
+
var SOURCE_TOKENS, DYNAMIC_DISPATCH_RE, DYNAMIC_METHOD_RE, BATCH_LOOP_RE, ROUTE_MATCH_RE, NORMALIZERS;
|
|
37188
|
+
var init_route_confusion = __esm(() => {
|
|
37189
|
+
SOURCE_TOKENS = /\$_(GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|\$request->|\$req->|request\.(get|input|query|post)|getParameter\(|req\.(query|body|params)/;
|
|
37190
|
+
DYNAMIC_DISPATCH_RE = /\b(call_user_func|call_user_func_array|forward|dispatch|resolve_route|invoke)\s*\(\s*(\$|\[)/gi;
|
|
37191
|
+
DYNAMIC_METHOD_RE = /(->|::)\s*\$[A-Za-z_]/g;
|
|
37192
|
+
BATCH_LOOP_RE = /\bforeach\s*\([^)]*(requests|batch|sub_requests|operations)\b/gi;
|
|
37193
|
+
ROUTE_MATCH_RE = /\b(preg_match|preg_match_all|strpos|stripos|str_contains)\s*\(/i;
|
|
37194
|
+
NORMALIZERS = /\b(strtolower|strtoupper|rtrim|urldecode|rawurldecode|parse_url|normalize|canonical)\b/i;
|
|
37195
|
+
});
|
|
37196
|
+
|
|
37197
|
+
// src/complex-bugs.ts
|
|
37198
|
+
function lines2(code) {
|
|
37199
|
+
return code.split(`
|
|
37200
|
+
`);
|
|
37201
|
+
}
|
|
37202
|
+
function lineNo2(code, pos) {
|
|
37203
|
+
let n = 1;
|
|
37204
|
+
for (let i = 0;i < pos; i++)
|
|
37205
|
+
if (code.charCodeAt(i) === 10)
|
|
37206
|
+
n++;
|
|
37207
|
+
return n;
|
|
37208
|
+
}
|
|
37209
|
+
function detectDeserialization(code, file, out) {
|
|
37210
|
+
if (!MAGIC_METHODS.test(code))
|
|
37211
|
+
return;
|
|
37212
|
+
const ls = lines2(code);
|
|
37213
|
+
const re = /\b(unserialize|maybe_unserialize)\s*\(/g;
|
|
37214
|
+
let m;
|
|
37215
|
+
while ((m = re.exec(code)) !== null) {
|
|
37216
|
+
const ln = lineNo2(code, m.index);
|
|
37217
|
+
const l = ls[ln - 1] ?? "";
|
|
37218
|
+
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
37219
|
+
`);
|
|
37220
|
+
if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
|
|
37221
|
+
continue;
|
|
37222
|
+
out.push({
|
|
37223
|
+
file,
|
|
37224
|
+
line: ln,
|
|
37225
|
+
type: "deserialization",
|
|
37226
|
+
category: "Deserialization → POP gadget chain",
|
|
37227
|
+
severity: "high",
|
|
37228
|
+
evidence: l.trim(),
|
|
37229
|
+
detail: `\`${m[1]}()\` of attacker-controlled data AND a magic method is present — object injection can pivot to RCE via the gadget chain.`
|
|
37230
|
+
});
|
|
37231
|
+
}
|
|
37232
|
+
}
|
|
37233
|
+
function detectTypeJuggling(code, file, out) {
|
|
37234
|
+
const ls = lines2(code);
|
|
37235
|
+
const re = /(?<![=!<>])(==|!=)(?!=)/g;
|
|
37236
|
+
let m;
|
|
37237
|
+
while ((m = re.exec(code)) !== null) {
|
|
37238
|
+
const ln = lineNo2(code, m.index);
|
|
37239
|
+
const l = ls[ln - 1] ?? "";
|
|
37240
|
+
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
37241
|
+
`);
|
|
37242
|
+
const hasSource = SOURCE_TOKENS2.test(l) || SOURCE_TOKENS2.test(scope);
|
|
37243
|
+
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);
|
|
37244
|
+
if (!hasSource || !hasHash)
|
|
37245
|
+
continue;
|
|
37246
|
+
out.push({
|
|
37247
|
+
file,
|
|
37248
|
+
line: ln,
|
|
37249
|
+
type: "type_juggling",
|
|
37250
|
+
category: "Type juggling (loose comparison)",
|
|
37251
|
+
severity: "medium",
|
|
37252
|
+
evidence: l.trim(),
|
|
37253
|
+
detail: 'Loose comparison of user input against a hash/secret — PHP type juggling (`0e…`, `"str" == 0`) may bypass the check. Use strict `===`.'
|
|
37254
|
+
});
|
|
37255
|
+
}
|
|
37256
|
+
}
|
|
37257
|
+
function detectMassAssignment(code, file, out) {
|
|
37258
|
+
const ls = lines2(code);
|
|
37259
|
+
const extractRe = /\bextract\s*\(\s*[^)]*(REQUEST|POST|GET|COOKIE|FILES|\$_(REQUEST|POST|GET|COOKIE|FILES))/g;
|
|
37260
|
+
let m;
|
|
37261
|
+
while ((m = extractRe.exec(code)) !== null) {
|
|
37262
|
+
const ln = lineNo2(code, m.index);
|
|
37263
|
+
const l = ls[ln - 1] ?? "";
|
|
37264
|
+
if (/EXTR_SKIP|EXTR_PREFIX_ALL/.test(l))
|
|
37265
|
+
continue;
|
|
37266
|
+
out.push({
|
|
37267
|
+
file,
|
|
37268
|
+
line: ln,
|
|
37269
|
+
type: "mass_assignment",
|
|
37270
|
+
category: "Mass assignment (variable injection)",
|
|
37271
|
+
severity: "high",
|
|
37272
|
+
evidence: l.trim(),
|
|
37273
|
+
detail: "`extract()` of request data without EXTR_SKIP — attacker can overwrite arbitrary variables."
|
|
37274
|
+
});
|
|
37275
|
+
}
|
|
37276
|
+
const parseRe = /\bparse_str\s*\(/g;
|
|
37277
|
+
while ((m = parseRe.exec(code)) !== null) {
|
|
37278
|
+
const ln = lineNo2(code, m.index);
|
|
37279
|
+
const l = ls[ln - 1] ?? "";
|
|
37280
|
+
const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
|
|
37281
|
+
`);
|
|
37282
|
+
if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
|
|
37283
|
+
continue;
|
|
37284
|
+
out.push({
|
|
37285
|
+
file,
|
|
37286
|
+
line: ln,
|
|
37287
|
+
type: "mass_assignment",
|
|
37288
|
+
category: "Mass assignment (variable injection)",
|
|
37289
|
+
severity: "medium",
|
|
37290
|
+
evidence: l.trim(),
|
|
37291
|
+
detail: "`parse_str()` of user input into the symbol table — variable injection."
|
|
37292
|
+
});
|
|
37293
|
+
}
|
|
37294
|
+
}
|
|
37295
|
+
function detectComplexBugs(code, file) {
|
|
37296
|
+
const out = [];
|
|
37297
|
+
detectDeserialization(code, file, out);
|
|
37298
|
+
detectTypeJuggling(code, file, out);
|
|
37299
|
+
detectMassAssignment(code, file, out);
|
|
37300
|
+
const seen = new Set;
|
|
37301
|
+
return out.filter((f) => {
|
|
37302
|
+
const key = `${f.type}:${f.line}`;
|
|
37303
|
+
if (seen.has(key))
|
|
37304
|
+
return false;
|
|
37305
|
+
seen.add(key);
|
|
37306
|
+
return true;
|
|
37307
|
+
});
|
|
37308
|
+
}
|
|
37309
|
+
var SOURCE_TOKENS2, MAGIC_METHODS;
|
|
37310
|
+
var init_complex_bugs = __esm(() => {
|
|
37311
|
+
SOURCE_TOKENS2 = /\$_(GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|\$request->|\$req->|request\.(get|input|query|post|data)|getParameter\(|req\.(query|body|params)/;
|
|
37312
|
+
MAGIC_METHODS = /\b(__destruct|__wakeup|__toString|__invoke|__call|__get|__set|__isset|__unset|__sleep)\s*\(/;
|
|
37313
|
+
});
|
|
37314
|
+
|
|
37053
37315
|
// src/agents.ts
|
|
37054
37316
|
import { readFileSync as readFileSync18 } from "node:fs";
|
|
37055
37317
|
function emptyGraph() {
|
|
@@ -37073,6 +37335,8 @@ var init_agents = __esm(() => {
|
|
|
37073
37335
|
init_scanner();
|
|
37074
37336
|
init_eagle2();
|
|
37075
37337
|
init_universal_taint();
|
|
37338
|
+
init_route_confusion();
|
|
37339
|
+
init_complex_bugs();
|
|
37076
37340
|
init_adapters();
|
|
37077
37341
|
init_active();
|
|
37078
37342
|
init_strike();
|
|
@@ -37157,6 +37421,44 @@ var init_agents = __esm(() => {
|
|
|
37157
37421
|
graph = addEdge2(graph, tid, sinkId, "flows_to");
|
|
37158
37422
|
}
|
|
37159
37423
|
}
|
|
37424
|
+
for (const f of files.filter((x) => /\.php$/i.test(x))) {
|
|
37425
|
+
let code;
|
|
37426
|
+
try {
|
|
37427
|
+
code = readFileSync18(f, "utf8");
|
|
37428
|
+
} catch {
|
|
37429
|
+
continue;
|
|
37430
|
+
}
|
|
37431
|
+
for (const rc of detectRouteConfusion(code, f).slice(0, 50)) {
|
|
37432
|
+
const hyp = makeHypothesis({
|
|
37433
|
+
type: rc.category,
|
|
37434
|
+
target: `${f}:${rc.line}`,
|
|
37435
|
+
evidence: rc.detail,
|
|
37436
|
+
confidence: rc.severity === "high" ? 0.7 : 0.5,
|
|
37437
|
+
required_validation: "confirm reachability of the dispatch/route with attacker-controlled input",
|
|
37438
|
+
potential_impact: "route confusion / authorization bypass"
|
|
37439
|
+
});
|
|
37440
|
+
hypotheses.push(hyp);
|
|
37441
|
+
findings += 1;
|
|
37442
|
+
const nodeId = `route:${f}:${rc.line}:${rc.type}`;
|
|
37443
|
+
graph = upsertNode(graph, nodeId, "route", { file: f, line: rc.line, type: rc.type, hypothesis: hyp.id });
|
|
37444
|
+
graph = addEdge2(graph, tid, nodeId, "routes_to");
|
|
37445
|
+
}
|
|
37446
|
+
for (const cb of detectComplexBugs(code, f).slice(0, 50)) {
|
|
37447
|
+
const hyp = makeHypothesis({
|
|
37448
|
+
type: cb.category,
|
|
37449
|
+
target: `${f}:${cb.line}`,
|
|
37450
|
+
evidence: cb.detail,
|
|
37451
|
+
confidence: cb.severity === "high" ? 0.7 : 0.55,
|
|
37452
|
+
required_validation: "confirm the payload reaches the sink and is exploitable",
|
|
37453
|
+
potential_impact: cb.type
|
|
37454
|
+
});
|
|
37455
|
+
hypotheses.push(hyp);
|
|
37456
|
+
findings += 1;
|
|
37457
|
+
const nodeId = `complex:${f}:${cb.line}:${cb.type}`;
|
|
37458
|
+
graph = upsertNode(graph, nodeId, "vulnerability", { file: f, line: cb.line, type: cb.type, hypothesis: hyp.id });
|
|
37459
|
+
graph = addEdge2(graph, tid, nodeId, "vulnerable_to");
|
|
37460
|
+
}
|
|
37461
|
+
}
|
|
37160
37462
|
return { ok: true, output: { files: files.length, findings }, hypotheses, graph };
|
|
37161
37463
|
}
|
|
37162
37464
|
};
|
|
@@ -38092,6 +38394,20 @@ var init_enterprise_benchmark = __esm(() => {
|
|
|
38092
38394
|
expected_vulns: 2,
|
|
38093
38395
|
expected_sinks: ["sql_execution", "command_execution"],
|
|
38094
38396
|
safe_tokens: ["prepareStatement", "PreparedStatement"]
|
|
38397
|
+
},
|
|
38398
|
+
{
|
|
38399
|
+
id: "wp-batch-route",
|
|
38400
|
+
dir: join17(ROOT10, "wp-batch-route"),
|
|
38401
|
+
expected_vulns: 1,
|
|
38402
|
+
expected_sinks: ["Route confusion"],
|
|
38403
|
+
safe_tokens: []
|
|
38404
|
+
},
|
|
38405
|
+
{
|
|
38406
|
+
id: "php-complex",
|
|
38407
|
+
dir: join17(ROOT10, "php-complex"),
|
|
38408
|
+
expected_vulns: 4,
|
|
38409
|
+
expected_sinks: ["Deserialization", "Type juggling", "Mass assignment"],
|
|
38410
|
+
safe_tokens: []
|
|
38095
38411
|
}
|
|
38096
38412
|
];
|
|
38097
38413
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blitzstrike",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.63",
|
|
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": {
|
|
@@ -1,21 +0,0 @@
|
|
|
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
|
-
```
|
|
@@ -1,23 +0,0 @@
|
|
|
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
|
-
```
|