blitzstrike 1.0.58 → 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 +56 -0
- package/bench/corpus/wp-author-not-in-fixed.md +21 -0
- package/bench/corpus/wp-author-not-in-vuln.md +23 -0
- package/bench/enterprise/java-spring/src/UserController.java +35 -0
- package/bench/enterprise/js-express/db/connection.js +10 -0
- package/bench/enterprise/js-express/routes/users.js +32 -0
- package/bench/enterprise/php-laravel/app/Http/Controllers/ReportController.php +30 -0
- package/bench/enterprise/php-laravel/app/Http/Controllers/UserController.php +33 -0
- package/bench/enterprise/php-laravel/app/Models/User.php +10 -0
- package/bench/enterprise/py-django/app/views.py +35 -0
- package/dist/index.js +212 -21
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,62 @@ 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
|
+
|
|
36
|
+
## [1.0.59] — 2026-09-13
|
|
37
|
+
|
|
38
|
+
### Added — enterprise-grade detection + benchmark
|
|
39
|
+
|
|
40
|
+
- **Enterprise benchmark** (`src/mission/enterprise-benchmark.ts`, `bench/enterprise/`):
|
|
41
|
+
four realistic multi-file, framework-style fixtures (Laravel PHP, Express JS,
|
|
42
|
+
Django Python, Spring Java) with ORM/query-builder abstractions and
|
|
43
|
+
known-safe endpoints. `blitzstrike benchmark --enterprise` runs them; the
|
|
44
|
+
engine now scores **10/10 vulns, 0 false positives**.
|
|
45
|
+
|
|
46
|
+
### Fixed — framework source/sink coverage (found by the enterprise benchmark)
|
|
47
|
+
|
|
48
|
+
- **PHP** — `sourceOf` now recognizes framework accessors (`$request->input()`,
|
|
49
|
+
`->query()`, `->get()`, `->post()`, …) not just `$_GET`; `sinkClassOf` handles
|
|
50
|
+
static calls (`DB::select()`, `DB::raw()`); and **function/method-internal
|
|
51
|
+
sources** are now emitted (previously only top-level + tainted-argument flows
|
|
52
|
+
were detected, so Laravel controllers scored 0).
|
|
53
|
+
- **JS** — removed `require()`/`child_process` module-import false positives;
|
|
54
|
+
sink matching now keys on the callee (no nested-call double-count); and
|
|
55
|
+
PostgreSQL `$1` parameterized queries are recognized as safe.
|
|
56
|
+
- **Python** — added Django/DRF sources (`request.GET`, `request.POST`,
|
|
57
|
+
`request.data`, `request.query_params`).
|
|
58
|
+
- **Java** — added Spring/JAX-RS annotation sources (`@RequestParam`,
|
|
59
|
+
`@PathVariable`, `@RequestBody`, …) and tightened sink variable extraction so
|
|
60
|
+
the tainted variable is no longer pushed out of the window by SQL keywords.
|
|
61
|
+
- `@analysis` agent now imports the language adapters (multi-language detection
|
|
62
|
+
previously returned 0 in the CLI/MCP path).
|
|
63
|
+
|
|
8
64
|
## [1.0.58] — 2026-09-13
|
|
9
65
|
|
|
10
66
|
### Fixed — total-verification pass
|
|
@@ -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
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package com.example.controller;
|
|
2
|
+
|
|
3
|
+
import org.springframework.web.bind.annotation.*;
|
|
4
|
+
import java.sql.*;
|
|
5
|
+
|
|
6
|
+
@RestController
|
|
7
|
+
@RequestMapping("/api/users")
|
|
8
|
+
public class UserController {
|
|
9
|
+
|
|
10
|
+
// VULNERABLE: SQL string concatenation of user input
|
|
11
|
+
@GetMapping("/search")
|
|
12
|
+
public String search(@RequestParam String q) throws Exception {
|
|
13
|
+
Connection conn = DriverManager.getConnection("jdbc:postgresql://db/app");
|
|
14
|
+
Statement st = conn.createStatement();
|
|
15
|
+
ResultSet rs = st.executeQuery("SELECT * FROM users WHERE name LIKE '%" + q + "%'");
|
|
16
|
+
return rs.getString(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// SAFE: PreparedStatement (parameterized)
|
|
20
|
+
@GetMapping("/show")
|
|
21
|
+
public String show(@RequestParam String id) throws Exception {
|
|
22
|
+
Connection conn = DriverManager.getConnection("jdbc:postgresql://db/app");
|
|
23
|
+
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
|
|
24
|
+
ps.setString(1, id);
|
|
25
|
+
ResultSet rs = ps.executeQuery();
|
|
26
|
+
return rs.getString(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// VULNERABLE: command execution from user input
|
|
30
|
+
@GetMapping("/export")
|
|
31
|
+
public String export(@RequestParam String file) throws Exception {
|
|
32
|
+
Runtime.getRuntime().exec("tar -czf /tmp/export.tar.gz " + file);
|
|
33
|
+
return "ok";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const { Pool } = require('pg');
|
|
2
|
+
|
|
3
|
+
// Realistic enterprise DB connection module with a safe helper.
|
|
4
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
5
|
+
|
|
6
|
+
async function safeQuery(text, params) {
|
|
7
|
+
return pool.query(text, params);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
module.exports = { pool, safeQuery };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const { pool, safeQuery } = require('../db/connection');
|
|
4
|
+
|
|
5
|
+
// VULNERABLE: raw SQL string concatenation of user input
|
|
6
|
+
router.get('/search', async (req, res) => {
|
|
7
|
+
const q = req.query.q;
|
|
8
|
+
const rows = await pool.query(`SELECT * FROM users WHERE name LIKE '%${q}%'`);
|
|
9
|
+
res.json(rows);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// VULNERABLE: command execution from user input
|
|
13
|
+
router.get('/export', (req, res) => {
|
|
14
|
+
const file = req.query.file;
|
|
15
|
+
require('child_process').execSync(`tar -czf /tmp/export.tar.gz ${file}`);
|
|
16
|
+
res.json({ ok: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// SAFE: parameterized query
|
|
20
|
+
router.get('/show', async (req, res) => {
|
|
21
|
+
const id = req.query.id;
|
|
22
|
+
const rows = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
|
|
23
|
+
res.json(rows);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// SAFE: escaped output
|
|
27
|
+
router.get('/preview', (req, res) => {
|
|
28
|
+
const html = req.query.html;
|
|
29
|
+
res.send(`<div>${String(html).replace(/[<>&"]/g, '')}</div>`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
module.exports = router;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
namespace App\Http\Controllers;
|
|
3
|
+
|
|
4
|
+
use Illuminate\Http\Request;
|
|
5
|
+
|
|
6
|
+
class ReportController extends Controller
|
|
7
|
+
{
|
|
8
|
+
// VULNERABLE: command execution from user input
|
|
9
|
+
public function export(Request $request)
|
|
10
|
+
{
|
|
11
|
+
$file = $request->input('file');
|
|
12
|
+
system("tar -czf /tmp/export.tar.gz {$file}");
|
|
13
|
+
return response()->json(['ok' => true]);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// VULNERABLE: code execution from user input
|
|
17
|
+
public function render(Request $request)
|
|
18
|
+
{
|
|
19
|
+
$tpl = $request->input('tpl');
|
|
20
|
+
eval("return {$tpl};");
|
|
21
|
+
return response()->json(['ok' => true]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// SAFE: escaped output
|
|
25
|
+
public function preview(Request $request)
|
|
26
|
+
{
|
|
27
|
+
$html = $request->input('html');
|
|
28
|
+
return view('preview', ['content' => htmlspecialchars($html)]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
namespace App\Http\Controllers;
|
|
3
|
+
|
|
4
|
+
use Illuminate\Http\Request;
|
|
5
|
+
use Illuminate\Support\Facades\DB;
|
|
6
|
+
use App\Models\User;
|
|
7
|
+
|
|
8
|
+
class UserController extends Controller
|
|
9
|
+
{
|
|
10
|
+
// VULNERABLE: raw SQL interpolation of user input
|
|
11
|
+
public function search(Request $request)
|
|
12
|
+
{
|
|
13
|
+
$q = $request->input('q');
|
|
14
|
+
$users = DB::select("SELECT * FROM users WHERE name LIKE '%{$q}%'");
|
|
15
|
+
return response()->json($users);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// VULNERABLE: raw where with user input (query builder bypass)
|
|
19
|
+
public function filter(Request $request)
|
|
20
|
+
{
|
|
21
|
+
$col = $request->input('col');
|
|
22
|
+
$rows = DB::table('users')->whereRaw("{$col} = 1")->get();
|
|
23
|
+
return response()->json($rows);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// SAFE: Eloquent ORM (parameterized)
|
|
27
|
+
public function show(Request $request)
|
|
28
|
+
{
|
|
29
|
+
$id = $request->input('id');
|
|
30
|
+
$user = User::where('id', $id)->first();
|
|
31
|
+
return response()->json($user);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from django.http import JsonResponse
|
|
2
|
+
from django.db import connection
|
|
3
|
+
from django.shortcuts import render
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def search(request):
|
|
7
|
+
# VULNERABLE: raw SQL interpolation of user input
|
|
8
|
+
q = request.GET.get('q')
|
|
9
|
+
cursor = connection.cursor()
|
|
10
|
+
cursor.execute("SELECT * FROM users WHERE name LIKE '%%%s%%'" % q)
|
|
11
|
+
rows = cursor.fetchall()
|
|
12
|
+
return JsonResponse({'rows': rows})
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def export(request):
|
|
16
|
+
# VULNERABLE: command execution from user input
|
|
17
|
+
import os
|
|
18
|
+
file = request.GET.get('file')
|
|
19
|
+
os.system("tar -czf /tmp/export.tar.gz " + file)
|
|
20
|
+
return JsonResponse({'ok': True})
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def show(request):
|
|
24
|
+
# SAFE: Django ORM (parameterized)
|
|
25
|
+
from .models import User
|
|
26
|
+
uid = request.GET.get('id')
|
|
27
|
+
user = User.objects.filter(id=uid).first()
|
|
28
|
+
return JsonResponse({'user': str(user)})
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def preview(request):
|
|
32
|
+
# SAFE: escaped output
|
|
33
|
+
from django.utils.html import escape
|
|
34
|
+
html = request.GET.get('html')
|
|
35
|
+
return JsonResponse({'content': escape(html)})
|
package/dist/index.js
CHANGED
|
@@ -9160,7 +9160,7 @@ var init_dataflow = __esm(() => {
|
|
|
9160
9160
|
wp_remote_get: { id: "external_api", label: "External API data", attacker_controlled: false }
|
|
9161
9161
|
};
|
|
9162
9162
|
SINK_CLASS_MAP = [
|
|
9163
|
-
[/->query\(|\$wpdb->query|\$wpdb->get_var|\$wpdb->get_results|->whereRaw|->selectRaw/, { id: "sql_execution", category: "SQL execution", cwe: "CWE-89" }],
|
|
9163
|
+
[/->query\(|\$wpdb->query|\$wpdb->get_var|\$wpdb->get_results|->whereRaw|->selectRaw|DB::select|DB::raw|DB::statement|DB::insert|DB::update|DB::unprepared/, { id: "sql_execution", category: "SQL execution", cwe: "CWE-89" }],
|
|
9164
9164
|
[/\beval\(|\bassert\(|\bcreate_function\(|\bcall_user_func\(|\bpreg_replace\(/, { id: "code_execution", category: "Dynamic evaluation / code execution", cwe: "CWE-94" }],
|
|
9165
9165
|
[/\bsystem\(|\bexec\(|\bshell_exec\(|\bpassthru\(|\bproc_open\(|\bpopen\(/, { id: "command_execution", category: "Command execution", cwe: "CWE-78" }],
|
|
9166
9166
|
[/\bmove_uploaded_file\(|\bfile_put_contents\(|\bfwrite\(|\bfopen\(|\bunlink\(/, { id: "file_operations", category: "File operations", cwe: "CWE-434" }],
|
|
@@ -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;
|
|
@@ -17123,6 +17133,14 @@ function sourceOf2(node) {
|
|
|
17123
17133
|
return "getallheaders";
|
|
17124
17134
|
if (n === "file_get_contents" && JSON.stringify(node).includes("php://input"))
|
|
17125
17135
|
return "php://input";
|
|
17136
|
+
if (node.what?.kind === "propertylookup") {
|
|
17137
|
+
const method = node.what.offset?.name ?? node.what.offset?.value;
|
|
17138
|
+
const base = rootVarName2(node.what.what);
|
|
17139
|
+
const reqMethods = ["input", "query", "get", "post", "all", "json", "cookie", "file", "header", "only", "except", "server"];
|
|
17140
|
+
if (base && /^(request|req)$/i.test(base) && reqMethods.includes(String(method))) {
|
|
17141
|
+
return `$${base}->${method}()`;
|
|
17142
|
+
}
|
|
17143
|
+
}
|
|
17126
17144
|
}
|
|
17127
17145
|
return null;
|
|
17128
17146
|
}
|
|
@@ -17139,6 +17157,11 @@ function sinkClassOf2(call) {
|
|
|
17139
17157
|
token = `${baseStr}->${method}(`;
|
|
17140
17158
|
} else if (what.kind === "name") {
|
|
17141
17159
|
token = `${what.name}(`;
|
|
17160
|
+
} else if (what.kind === "staticlookup") {
|
|
17161
|
+
const clsName = nameOf2(what.what);
|
|
17162
|
+
const method = what.offset?.name ?? what.offset?.value;
|
|
17163
|
+
if (clsName && method)
|
|
17164
|
+
token = `${clsName}::${method}(`;
|
|
17142
17165
|
}
|
|
17143
17166
|
if (!token)
|
|
17144
17167
|
return null;
|
|
@@ -17170,8 +17193,16 @@ function summarizeFunction(fn, id, name, summaries) {
|
|
|
17170
17193
|
if (a.byref === true)
|
|
17171
17194
|
byrefParams.add(params[i]);
|
|
17172
17195
|
});
|
|
17173
|
-
const summary = { id, name, params, byrefParams, paramSinks: new Map, paramReturns: new Set, paramCalls: new Map, paramWrites: new Map };
|
|
17196
|
+
const summary = { id, name, params, byrefParams, paramSinks: new Map, paramReturns: new Set, paramCalls: new Map, paramWrites: new Map, internalSinks: [] };
|
|
17174
17197
|
const bodyStmts = fn.body?.kind === "block" ? fn.body.children ?? [] : [fn.body];
|
|
17198
|
+
{
|
|
17199
|
+
const cleanEnv = new Map;
|
|
17200
|
+
for (const p of params)
|
|
17201
|
+
cleanEnv.set(p, CLEAN);
|
|
17202
|
+
const cleanCtx = { sinks: [], returnsTainted: false, calls: [] };
|
|
17203
|
+
analyzeBody(bodyStmts, cleanEnv, summary, cleanCtx, "", new Map, [], summaries);
|
|
17204
|
+
summary.internalSinks = cleanCtx.sinks;
|
|
17205
|
+
}
|
|
17175
17206
|
for (const p of params) {
|
|
17176
17207
|
const env = new Map;
|
|
17177
17208
|
for (const q of params)
|
|
@@ -17247,6 +17278,14 @@ function analyzeExpr(node, env, summary, ctx, taintedParam, props, activeAuth, s
|
|
|
17247
17278
|
const src = sourceOf2(node);
|
|
17248
17279
|
if (src)
|
|
17249
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
|
+
}
|
|
17250
17289
|
if (node.kind === "assign") {
|
|
17251
17290
|
const byref = node.operator === "=&";
|
|
17252
17291
|
const rhs = analyzeExpr(node.right, env, summary, ctx, taintedParam, props, activeAuth, summaries, validating);
|
|
@@ -17294,8 +17333,11 @@ function analyzeExpr(node, env, summary, ctx, taintedParam, props, activeAuth, s
|
|
|
17294
17333
|
analyzeExpr(a, env, summary, ctx, taintedParam, props, activeAuth, summaries, validating);
|
|
17295
17334
|
return taintedArgs.length > 0 ? mergeTaint(...taintedArgs) : CLEAN;
|
|
17296
17335
|
}
|
|
17297
|
-
const fnName =
|
|
17336
|
+
const fnName = calleeName(node.what);
|
|
17298
17337
|
if (fnName) {
|
|
17338
|
+
if (TAINT_PROPAGATING_BUILTINS.has(fnName) && taintedArgs.length > 0) {
|
|
17339
|
+
return mergeTaint(...taintedArgs);
|
|
17340
|
+
}
|
|
17299
17341
|
const san = findSanitizers(fnName + "(");
|
|
17300
17342
|
if (san.length > 0) {
|
|
17301
17343
|
if (taintedArgs.length > 0) {
|
|
@@ -17525,7 +17567,7 @@ function analyzeDataFlow2(path) {
|
|
|
17525
17567
|
if (isNode3(s) && s.kind !== "function" && s.kind !== "class")
|
|
17526
17568
|
topStmts.push(s);
|
|
17527
17569
|
}
|
|
17528
|
-
const mainSummary = { id: "main", name: "main", params: [], byrefParams: new Set, paramSinks: new Map, paramReturns: new Set, paramCalls: new Map, paramWrites: new Map };
|
|
17570
|
+
const mainSummary = { id: "main", name: "main", params: [], byrefParams: new Set, paramSinks: new Map, paramReturns: new Set, paramCalls: new Map, paramWrites: new Map, internalSinks: [] };
|
|
17529
17571
|
const mainEnv = new Map;
|
|
17530
17572
|
const mainCtx = { sinks: [], returnsTainted: false, calls: [] };
|
|
17531
17573
|
const mainProps = new Map;
|
|
@@ -17554,7 +17596,7 @@ function analyzeDataFlow2(path) {
|
|
|
17554
17596
|
const seen = new Set;
|
|
17555
17597
|
const mainCalls = topStmts.flatMap((s) => findKind2(s, "call"));
|
|
17556
17598
|
for (const call of mainCalls) {
|
|
17557
|
-
const fnName =
|
|
17599
|
+
const fnName = calleeName(call.what);
|
|
17558
17600
|
if (!fnName || sinkClassOf2(call))
|
|
17559
17601
|
continue;
|
|
17560
17602
|
const target = resolveFuncId(fnName, summaries);
|
|
@@ -17609,11 +17651,39 @@ function analyzeDataFlow2(path) {
|
|
|
17609
17651
|
queue.push({ funcId: calleeId, param: calleeParam, source: item.source, sourceLine: item.sourceLine, viaReturn: false });
|
|
17610
17652
|
}
|
|
17611
17653
|
}
|
|
17654
|
+
const seenInternal = new Set;
|
|
17655
|
+
for (const sum of summaries.values()) {
|
|
17656
|
+
for (const hit of sum.internalSinks) {
|
|
17657
|
+
const cls = classifySink2(hit.sink);
|
|
17658
|
+
if (!cls)
|
|
17659
|
+
continue;
|
|
17660
|
+
const key = `${hit.sink}:${hit.line}`;
|
|
17661
|
+
if (seenInternal.has(key))
|
|
17662
|
+
continue;
|
|
17663
|
+
seenInternal.add(key);
|
|
17664
|
+
if (hit.sanitized) {
|
|
17665
|
+
result.suppressed += 1;
|
|
17666
|
+
continue;
|
|
17667
|
+
}
|
|
17668
|
+
result.findings.push({
|
|
17669
|
+
sink: hit.sink,
|
|
17670
|
+
sink_line: hit.line,
|
|
17671
|
+
category: cls.category,
|
|
17672
|
+
cwe: cls.cwe,
|
|
17673
|
+
sanitized: false,
|
|
17674
|
+
auth_gated: hit.authGated,
|
|
17675
|
+
auth_gates: hit.authGates,
|
|
17676
|
+
interprocedural: false,
|
|
17677
|
+
via_return: false,
|
|
17678
|
+
internal_source: true
|
|
17679
|
+
});
|
|
17680
|
+
}
|
|
17681
|
+
}
|
|
17612
17682
|
if (cacheCode)
|
|
17613
17683
|
setCache("eagle2", cacheCode, result);
|
|
17614
17684
|
return result;
|
|
17615
17685
|
}
|
|
17616
|
-
var import_php_parser3, parser3, CLEAN, VALIDATION_GATES;
|
|
17686
|
+
var import_php_parser3, parser3, TAINT_PROPAGATING_BUILTINS, CLEAN, VALIDATION_GATES;
|
|
17617
17687
|
var init_eagle2 = __esm(() => {
|
|
17618
17688
|
init_dataflow();
|
|
17619
17689
|
init_scanner();
|
|
@@ -17624,6 +17694,33 @@ var init_eagle2 = __esm(() => {
|
|
|
17624
17694
|
parser: { extractDoc: false, php7: true },
|
|
17625
17695
|
ast: { withPositions: true, withSource: false }
|
|
17626
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
|
+
]);
|
|
17627
17724
|
CLEAN = { tainted: false, sources: [], sanitizers: [] };
|
|
17628
17725
|
VALIDATION_GATES = ["in_array", "preg_match", "ctype_digit", "ctype_alnum", "is_numeric"];
|
|
17629
17726
|
});
|
|
@@ -35599,7 +35696,6 @@ var init_adapters = __esm(() => {
|
|
|
35599
35696
|
[/\.query\s*\(/i, "sql_execution", "CWE-89"],
|
|
35600
35697
|
[/eval\s*\(/i, "code_execution", "CWE-94"],
|
|
35601
35698
|
[/exec\s*\(/i, "command_execution", "CWE-78"],
|
|
35602
|
-
[/child_process/i, "command_execution", "CWE-78"],
|
|
35603
35699
|
[/execSync\s*\(/i, "command_execution", "CWE-78"],
|
|
35604
35700
|
[/\.send\s*\(/i, "html_render", "CWE-79"],
|
|
35605
35701
|
[/innerHTML/i, "html_render", "CWE-79"],
|
|
@@ -35609,7 +35705,6 @@ var init_adapters = __esm(() => {
|
|
|
35609
35705
|
[/fs\.readFile/i, "path_traversal", "CWE-22"],
|
|
35610
35706
|
[/fs\.readFileSync/i, "path_traversal", "CWE-22"],
|
|
35611
35707
|
[/fs\.unlink/i, "file_operations", "CWE-434"],
|
|
35612
|
-
[/require\s*\(/i, "file_inclusion", "CWE-98"],
|
|
35613
35708
|
[/fetch\s*\(/i, "http_request", "CWE-918"],
|
|
35614
35709
|
[/http\.get|https\.get|axios/i, "http_request", "CWE-918"],
|
|
35615
35710
|
[/http\.request|https\.request/i, "http_request", "CWE-918"],
|
|
@@ -35692,8 +35787,8 @@ var init_adapters = __esm(() => {
|
|
|
35692
35787
|
if (c.type === "Identifier" && c.name)
|
|
35693
35788
|
argVars.push(c.name);
|
|
35694
35789
|
for (const [re, kind, cwe] of JS_SINKS) {
|
|
35695
|
-
if (re.test(
|
|
35696
|
-
if (kind === "sql_execution" &&
|
|
35790
|
+
if (re.test(calleeText + "(")) {
|
|
35791
|
+
if (kind === "sql_execution" && /(\$\d+|\?)/.test(fullCallText) && /,\s*\[/.test(fullCallText))
|
|
35697
35792
|
continue;
|
|
35698
35793
|
for (const v of argVars)
|
|
35699
35794
|
sinks.push({ variable: v, kind, line: n.loc?.start?.line ?? 0, cwe });
|
|
@@ -35719,6 +35814,10 @@ var init_adapters = __esm(() => {
|
|
|
35719
35814
|
[/request\.cookies/i, "http_cookie"],
|
|
35720
35815
|
[/request\.files/i, "uploaded_file"],
|
|
35721
35816
|
[/request\.get/i, "http_parameter"],
|
|
35817
|
+
[/request\.GET/i, "http_parameter"],
|
|
35818
|
+
[/request\.POST/i, "http_body"],
|
|
35819
|
+
[/request\.data/i, "http_body"],
|
|
35820
|
+
[/request\.query_params/i, "http_parameter"],
|
|
35722
35821
|
[/sys\.argv/i, "cli_argument"],
|
|
35723
35822
|
[/os\.environ/i, "env_variable"]
|
|
35724
35823
|
];
|
|
@@ -35900,6 +35999,14 @@ var init_adapters = __esm(() => {
|
|
|
35900
35999
|
sources.push({ variable: varName, kind: src[1], line, attacker_controlled: src[1] !== "cli_argument" });
|
|
35901
36000
|
assigns.push({ target: varName, sources: [], line });
|
|
35902
36001
|
}
|
|
36002
|
+
const annotRe = /@(?:RequestParam|PathVariable|RequestBody|RequestHeader|CookieValue)(?:\([^)]*\))?\s+(?:[\w<>,\.\[\]\s]+\s+)?([A-Za-z_]\w*)/g;
|
|
36003
|
+
while ((m = annotRe.exec(code)) !== null) {
|
|
36004
|
+
const varName = m[1];
|
|
36005
|
+
const line = code.slice(0, m.index).split(`
|
|
36006
|
+
`).length;
|
|
36007
|
+
sources.push({ variable: varName, kind: "http_parameter", line, attacker_controlled: true });
|
|
36008
|
+
assigns.push({ target: varName, sources: [], line });
|
|
36009
|
+
}
|
|
35903
36010
|
for (let i = 0;i < lines.length; i++) {
|
|
35904
36011
|
const line = lines[i];
|
|
35905
36012
|
for (const [re, kind, cwe] of JAVA_SINKS) {
|
|
@@ -35908,9 +36015,12 @@ var init_adapters = __esm(() => {
|
|
|
35908
36015
|
const vre = /([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
35909
36016
|
let vm;
|
|
35910
36017
|
while ((vm = vre.exec(line)) !== null) {
|
|
35911
|
-
|
|
35912
|
-
|
|
35913
|
-
|
|
36018
|
+
const id = vm[1];
|
|
36019
|
+
if (/Runtime|getRuntime|ProcessBuilder|ObjectInputStream|HtmlUtils|StringEscapeUtils|PreparedStatement|Integer|Long|sendRedirect|forward|executeQuery|execute|exec|createStatement|getConnection|prepareStatement|getString|DriverManager/.test(id))
|
|
36020
|
+
continue;
|
|
36021
|
+
if (/^[A-Z]/.test(id))
|
|
36022
|
+
continue;
|
|
36023
|
+
argVars.push(id);
|
|
35914
36024
|
}
|
|
35915
36025
|
for (const v of argVars.slice(0, 8))
|
|
35916
36026
|
sinks.push({ variable: v, kind, line: i + 1, cwe });
|
|
@@ -36963,6 +37073,7 @@ var init_agents = __esm(() => {
|
|
|
36963
37073
|
init_scanner();
|
|
36964
37074
|
init_eagle2();
|
|
36965
37075
|
init_universal_taint();
|
|
37076
|
+
init_adapters();
|
|
36966
37077
|
init_active();
|
|
36967
37078
|
init_strike();
|
|
36968
37079
|
reconAgent = {
|
|
@@ -37912,13 +38023,86 @@ var init_cli = __esm(() => {
|
|
|
37912
38023
|
VALID_MODES = ["web", "api", "source", "mobile", "cloud", "bugbounty", "redteam", "audit"];
|
|
37913
38024
|
});
|
|
37914
38025
|
|
|
38026
|
+
// src/mission/enterprise-benchmark.ts
|
|
38027
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
38028
|
+
import { join as join17, resolve } from "node:path";
|
|
38029
|
+
async function runEnterpriseBenchmark() {
|
|
38030
|
+
const fixtures = [];
|
|
38031
|
+
for (const fx of ENTERPRISE_FIXTURES) {
|
|
38032
|
+
if (!existsSync11(fx.dir)) {
|
|
38033
|
+
fixtures.push({ id: fx.id, available: false, findings: [], detected_vulns: 0, expected_vulns: fx.expected_vulns, recall: 0, false_positive_tokens: [] });
|
|
38034
|
+
continue;
|
|
38035
|
+
}
|
|
38036
|
+
const m = await startMission(fx.dir, "source", { scope: [] });
|
|
38037
|
+
const findings = m.hypotheses.filter((h) => h.type !== "missing_authorization" && h.type !== "auth_flow_detected").map((h) => h.type);
|
|
38038
|
+
const falsePositives = fx.safe_tokens.filter((tok) => m.hypotheses.some((h) => h.target.includes(tok) || h.evidence.includes(tok)));
|
|
38039
|
+
fixtures.push({
|
|
38040
|
+
id: fx.id,
|
|
38041
|
+
available: true,
|
|
38042
|
+
findings,
|
|
38043
|
+
detected_vulns: findings.length,
|
|
38044
|
+
expected_vulns: fx.expected_vulns,
|
|
38045
|
+
recall: fx.expected_vulns > 0 ? findings.length / fx.expected_vulns : 0,
|
|
38046
|
+
false_positive_tokens: falsePositives
|
|
38047
|
+
});
|
|
38048
|
+
}
|
|
38049
|
+
const totalDetected = fixtures.filter((f) => f.available).reduce((s, f) => s + f.detected_vulns, 0);
|
|
38050
|
+
const totalExpected = fixtures.filter((f) => f.available).reduce((s, f) => s + f.expected_vulns, 0);
|
|
38051
|
+
const totalFP = fixtures.filter((f) => f.available).reduce((s, f) => s + f.false_positive_tokens.length, 0);
|
|
38052
|
+
return {
|
|
38053
|
+
fixtures,
|
|
38054
|
+
summary: {
|
|
38055
|
+
fixtures: fixtures.filter((f) => f.available).length,
|
|
38056
|
+
detected_vulns: totalDetected,
|
|
38057
|
+
expected_vulns: totalExpected,
|
|
38058
|
+
recall: Number((totalExpected > 0 ? totalDetected / totalExpected : 0).toFixed(2)),
|
|
38059
|
+
false_positives: totalFP
|
|
38060
|
+
}
|
|
38061
|
+
};
|
|
38062
|
+
}
|
|
38063
|
+
var ROOT10, ENTERPRISE_FIXTURES;
|
|
38064
|
+
var init_enterprise_benchmark = __esm(() => {
|
|
38065
|
+
init_control();
|
|
38066
|
+
ROOT10 = resolve(process.cwd(), "bench", "enterprise");
|
|
38067
|
+
ENTERPRISE_FIXTURES = [
|
|
38068
|
+
{
|
|
38069
|
+
id: "php-laravel",
|
|
38070
|
+
dir: join17(ROOT10, "php-laravel"),
|
|
38071
|
+
expected_vulns: 4,
|
|
38072
|
+
expected_sinks: ["DB::select", "whereRaw", "system", "eval"],
|
|
38073
|
+
safe_tokens: ["User::where", "htmlspecialchars"]
|
|
38074
|
+
},
|
|
38075
|
+
{
|
|
38076
|
+
id: "js-express",
|
|
38077
|
+
dir: join17(ROOT10, "js-express"),
|
|
38078
|
+
expected_vulns: 2,
|
|
38079
|
+
expected_sinks: ["sql_execution", "command_execution"],
|
|
38080
|
+
safe_tokens: ["$1", "replace"]
|
|
38081
|
+
},
|
|
38082
|
+
{
|
|
38083
|
+
id: "py-django",
|
|
38084
|
+
dir: join17(ROOT10, "py-django"),
|
|
38085
|
+
expected_vulns: 2,
|
|
38086
|
+
expected_sinks: ["sql_execution", "command_execution"],
|
|
38087
|
+
safe_tokens: ["filter", "escape"]
|
|
38088
|
+
},
|
|
38089
|
+
{
|
|
38090
|
+
id: "java-spring",
|
|
38091
|
+
dir: join17(ROOT10, "java-spring"),
|
|
38092
|
+
expected_vulns: 2,
|
|
38093
|
+
expected_sinks: ["sql_execution", "command_execution"],
|
|
38094
|
+
safe_tokens: ["prepareStatement", "PreparedStatement"]
|
|
38095
|
+
}
|
|
38096
|
+
];
|
|
38097
|
+
});
|
|
38098
|
+
|
|
37915
38099
|
// src/mission/benchmark.ts
|
|
37916
38100
|
import { writeFileSync as writeFileSync7, rmSync as rmSync5, mkdtempSync as mkdtempSync2 } from "node:fs";
|
|
37917
|
-
import { join as
|
|
38101
|
+
import { join as join18 } from "node:path";
|
|
37918
38102
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
37919
38103
|
async function runOne(id, vulnerable, language, code) {
|
|
37920
|
-
const dir = mkdtempSync2(
|
|
37921
|
-
const file =
|
|
38104
|
+
const dir = mkdtempSync2(join18(tmpdir3(), "blitz-auto-"));
|
|
38105
|
+
const file = join18(dir, `sample${EXT[language] ?? ".txt"}`);
|
|
37922
38106
|
writeFileSync7(file, code + `
|
|
37923
38107
|
`);
|
|
37924
38108
|
const t0 = Date.now();
|
|
@@ -55685,14 +55869,14 @@ Registered with ${ok}/${installed.length} agent(s).`);
|
|
|
55685
55869
|
}
|
|
55686
55870
|
|
|
55687
55871
|
// src/index.ts
|
|
55688
|
-
import { readFileSync as readFileSync23, existsSync as
|
|
55689
|
-
import { join as
|
|
55872
|
+
import { readFileSync as readFileSync23, existsSync as existsSync12 } from "node:fs";
|
|
55873
|
+
import { join as join19 } from "node:path";
|
|
55690
55874
|
import { fileURLToPath as fileURLToPath12 } from "node:url";
|
|
55691
|
-
var
|
|
55875
|
+
var ROOT11 = join19(fileURLToPath12(new URL(".", import.meta.url)), "..");
|
|
55692
55876
|
function readVersion() {
|
|
55693
55877
|
try {
|
|
55694
|
-
const p =
|
|
55695
|
-
if (
|
|
55878
|
+
const p = join19(ROOT11, "package.json");
|
|
55879
|
+
if (existsSync12(p))
|
|
55696
55880
|
return JSON.parse(readFileSync23(p, "utf8")).version ?? "1.0.0";
|
|
55697
55881
|
} catch {}
|
|
55698
55882
|
return "1.0.0";
|
|
@@ -55737,6 +55921,13 @@ async function main() {
|
|
|
55737
55921
|
return;
|
|
55738
55922
|
}
|
|
55739
55923
|
if (args[0] === "benchmark") {
|
|
55924
|
+
if (args.includes("--enterprise")) {
|
|
55925
|
+
await Promise.resolve().then(() => init_enterprise_benchmark());
|
|
55926
|
+
const r = await runEnterpriseBenchmark();
|
|
55927
|
+
console.log("Blitz Strike enterprise benchmark (framework-style fixtures)");
|
|
55928
|
+
console.log(JSON.stringify(r, null, 2));
|
|
55929
|
+
return;
|
|
55930
|
+
}
|
|
55740
55931
|
await Promise.resolve().then(() => init_benchmark2());
|
|
55741
55932
|
const limit = args.includes("--full") ? undefined : 10;
|
|
55742
55933
|
const r = await runAutonomousBenchmark(limit);
|
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": {
|