blitzstrike 2.4.59 → 2.4.61
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 +26 -0
- package/dist/index.js +1600 -1294
- package/intelligence/cwe_map.json +5 -1
- package/intelligence/patterns.json +36 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7385,6 +7385,69 @@ var init_version = __esm(() => {
|
|
|
7385
7385
|
})();
|
|
7386
7386
|
});
|
|
7387
7387
|
|
|
7388
|
+
// src/secrets.ts
|
|
7389
|
+
function shannonEntropy(s) {
|
|
7390
|
+
if (!s)
|
|
7391
|
+
return 0;
|
|
7392
|
+
const freq = new Map;
|
|
7393
|
+
for (const c of s)
|
|
7394
|
+
freq.set(c, (freq.get(c) ?? 0) + 1);
|
|
7395
|
+
let e = 0;
|
|
7396
|
+
for (const n of freq.values()) {
|
|
7397
|
+
const p = n / s.length;
|
|
7398
|
+
e -= p * Math.log2(p);
|
|
7399
|
+
}
|
|
7400
|
+
return e;
|
|
7401
|
+
}
|
|
7402
|
+
function redactSecrets(text) {
|
|
7403
|
+
let out = text;
|
|
7404
|
+
for (const [re, repl] of REDACTION_PATTERNS) {
|
|
7405
|
+
out = out.replace(re, repl);
|
|
7406
|
+
}
|
|
7407
|
+
return out;
|
|
7408
|
+
}
|
|
7409
|
+
var SECRET_FIELDS = "api[_-]?key|apikey|api[_-]?secret|access[_-]?key|access[_-]?token|secret[_-]?key|secret[_-]?access[_-]?key|auth[_-]?token|client[_-]?secret|consumer[_-]?secret|consumer[_-]?key|private[_-]?key|db[_-]?password|database[_-]?password|db[_-]?pass|passwd|password|jwt[_-]?secret|encryption[_-]?key|encryption[_-]?password|github[_-]?token|gitlab[_-]?token|stripe[_-]?key|sendgrid[_-]?api[_-]?key|datadog[_-]?api[_-]?key|cloudflare[_-]?api[_-]?key|slack[_-]?token|aws[_-]?access[_-]?key[_-]?id|aws[_-]?secret[_-]?access[_-]?key|aws[_-]?session[_-]?token|bot[_-]?token", SECRET_FORMATS, REDACTION_PATTERNS;
|
|
7410
|
+
var init_secrets = __esm(() => {
|
|
7411
|
+
SECRET_FORMATS = [
|
|
7412
|
+
{ name: "JWT", re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}/g },
|
|
7413
|
+
{ name: "AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
7414
|
+
{ name: "GitHub PAT (classic)", re: /\bghp_[A-Za-z0-9]{20,}\b/g },
|
|
7415
|
+
{ name: "GitHub PAT (fine-grained)", re: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g },
|
|
7416
|
+
{ name: "OpenAI/Stripe key", re: /\bsk-[A-Za-z0-9]{20,}\b/g },
|
|
7417
|
+
{ name: "Stripe live/test key", re: /\b(?:sk|pk|rk)_(?:live|test)_[0-9a-zA-Z]{10,}\b/g },
|
|
7418
|
+
{ name: "Slack token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
|
|
7419
|
+
{ name: "Google API key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
7420
|
+
{ name: "Twilio account SID", re: /\bAC[0-9a-f]{32}\b/g },
|
|
7421
|
+
{ name: "SendGrid API key", re: /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g },
|
|
7422
|
+
{ name: "Mailgun API key", re: /\bkey-[0-9a-zA-Z]{32}\b/g },
|
|
7423
|
+
{ name: "PEM private key", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/g }
|
|
7424
|
+
];
|
|
7425
|
+
REDACTION_PATTERNS = [
|
|
7426
|
+
[/(authorization\s*[:=]\s*)(bearer\s+)?[A-Za-z0-9._~+/=-]{8,}/gi, "$1$2[REDACTED]"],
|
|
7427
|
+
[/(api[_-]?key\s*[:=]\s*)["']?[A-Za-z0-9._-]{16,}["']?/gi, "$1[REDACTED]"],
|
|
7428
|
+
[/(access[_-]?token\s*[:=]\s*)["']?[A-Za-z0-9._-]{16,}["']?/gi, "$1[REDACTED]"],
|
|
7429
|
+
[/(secret\s*[:=]\s*)["']?[A-Za-z0-9._/-]{16,}["']?/gi, "$1[REDACTED]"],
|
|
7430
|
+
[/(password\s*[:=]\s*)["']?[^"'\s,}&]{4,}["']?/gi, "$1[REDACTED]"],
|
|
7431
|
+
[/(passwd\s*[:=]\s*)["']?[^"'\s,}&]{4,}["']?/gi, "$1[REDACTED]"],
|
|
7432
|
+
[/(private[_-]?key\s*[:=]\s*)["']?[A-Za-z0-9+/=_-]{16,}["']?/gi, "$1[REDACTED]"],
|
|
7433
|
+
[/(session\s*[:=]\s*)["']?[A-Za-z0-9._-]{12,}["']?/gi, "$1[REDACTED]"],
|
|
7434
|
+
[/(cookie\s*[:=]\s*)["']?[A-Za-z0-9._-]{12,}["']?/gi, "$1[REDACTED]"],
|
|
7435
|
+
[/(set-cookie\s*:\s*)[^\r\n]+/gi, "$1[REDACTED]"],
|
|
7436
|
+
[/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}/g, "[REDACTED]"],
|
|
7437
|
+
[/AKIA[0-9A-Z]{16}/g, "[REDACTED]"],
|
|
7438
|
+
[/ghp_[A-Za-z0-9]{20,}/g, "[REDACTED]"],
|
|
7439
|
+
[/github_pat_[A-Za-z0-9_]{20,}/g, "[REDACTED]"],
|
|
7440
|
+
[/sk-[A-Za-z0-9]{20,}/g, "[REDACTED]"],
|
|
7441
|
+
[/\b(?:sk|pk|rk)_(?:live|test)_[0-9a-zA-Z]{10,}\b/g, "[REDACTED]"],
|
|
7442
|
+
[/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, "[REDACTED]"],
|
|
7443
|
+
[/\bAIza[0-9A-Za-z_-]{35}\b/g, "[REDACTED]"],
|
|
7444
|
+
[/\bAC[0-9a-f]{32}\b/g, "[REDACTED]"],
|
|
7445
|
+
[/\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g, "[REDACTED]"],
|
|
7446
|
+
[/\bkey-[0-9a-zA-Z]{32}\b/g, "[REDACTED]"],
|
|
7447
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[REDACTED]"]
|
|
7448
|
+
];
|
|
7449
|
+
});
|
|
7450
|
+
|
|
7388
7451
|
// src/http.ts
|
|
7389
7452
|
function researchHeaders() {
|
|
7390
7453
|
const u = process.env.H1_USERNAME?.trim();
|
|
@@ -15489,256 +15552,1451 @@ async function checkMcpServers() {
|
|
|
15489
15552
|
}
|
|
15490
15553
|
var init_mcp_status = () => {};
|
|
15491
15554
|
|
|
15492
|
-
// src/
|
|
15493
|
-
|
|
15494
|
-
|
|
15495
|
-
|
|
15496
|
-
|
|
15497
|
-
|
|
15498
|
-
|
|
15499
|
-
|
|
15500
|
-
|
|
15501
|
-
|
|
15502
|
-
|
|
15503
|
-
|
|
15504
|
-
}
|
|
15505
|
-
function countFiles(dir) {
|
|
15506
|
-
try {
|
|
15507
|
-
let n = 0;
|
|
15508
|
-
const walk = (d) => {
|
|
15509
|
-
for (const e of readdirSync7(d, { withFileTypes: true })) {
|
|
15510
|
-
if (e.isDirectory())
|
|
15511
|
-
walk(join21(d, e.name));
|
|
15512
|
-
else
|
|
15513
|
-
n++;
|
|
15555
|
+
// src/security-state.ts
|
|
15556
|
+
function analyzeSecurityState(input) {
|
|
15557
|
+
let state = "tainted";
|
|
15558
|
+
let context = null;
|
|
15559
|
+
for (const fn of input.sanitizers) {
|
|
15560
|
+
const s = SANITIZERS2[fn];
|
|
15561
|
+
if (!s)
|
|
15562
|
+
continue;
|
|
15563
|
+
if (s.level === "validated") {
|
|
15564
|
+
if (state === "tainted" || state === "sanitized") {
|
|
15565
|
+
state = "validated";
|
|
15566
|
+
context = s.context;
|
|
15514
15567
|
}
|
|
15515
|
-
}
|
|
15516
|
-
|
|
15517
|
-
|
|
15518
|
-
|
|
15519
|
-
return 0;
|
|
15568
|
+
} else if (state === "tainted") {
|
|
15569
|
+
state = "sanitized";
|
|
15570
|
+
context = s.context;
|
|
15571
|
+
}
|
|
15520
15572
|
}
|
|
15521
|
-
|
|
15522
|
-
|
|
15523
|
-
|
|
15524
|
-
mkdirSync9(DATA_ROOT2, { recursive: true });
|
|
15525
|
-
const payloadsPath = join21(DATA_ROOT2, "payloads");
|
|
15526
|
-
const templatesPath = join21(DATA_ROOT2, "templates");
|
|
15527
|
-
const hasPayloads = dirExists(payloadsPath) && countFiles(payloadsPath) > 0;
|
|
15528
|
-
const hasTemplates = dirExists(templatesPath) && countFiles(templatesPath) > 0;
|
|
15529
|
-
if (hasPayloads && hasTemplates) {
|
|
15530
|
-
return {
|
|
15531
|
-
payloads: { path: payloadsPath, files: countFiles(payloadsPath), synced: false },
|
|
15532
|
-
templates: { path: templatesPath, files: countFiles(templatesPath), synced: false },
|
|
15533
|
-
data_root: DATA_ROOT2
|
|
15534
|
-
};
|
|
15573
|
+
const required = SINK_REQUIREMENTS[input.sinkType] ?? [];
|
|
15574
|
+
if (required.length === 0) {
|
|
15575
|
+
return { verdict: "unknown", state, context, required, reason: `sink type '${input.sinkType}' not in the requirements table` };
|
|
15535
15576
|
}
|
|
15536
|
-
|
|
15537
|
-
|
|
15538
|
-
let payloadsSynced = false;
|
|
15539
|
-
let templatesSynced = false;
|
|
15540
|
-
if (!hasPayloads && dirExists(localPayloads)) {
|
|
15541
|
-
copyDir(localPayloads, payloadsPath);
|
|
15542
|
-
payloadsSynced = true;
|
|
15577
|
+
if (state === "tainted") {
|
|
15578
|
+
return { verdict: "vulnerable", state, context, required, reason: `raw tainted input reaches a ${input.sinkType} sink with no sanitization` };
|
|
15543
15579
|
}
|
|
15544
|
-
if (
|
|
15545
|
-
|
|
15546
|
-
templatesSynced = true;
|
|
15580
|
+
if (state === "validated") {
|
|
15581
|
+
return { verdict: "safe", state, context, required, reason: `input validated (${context}) — safe for any sink` };
|
|
15547
15582
|
}
|
|
15548
|
-
|
|
15549
|
-
|
|
15550
|
-
|
|
15551
|
-
if (dirExists(tmp))
|
|
15552
|
-
rmSync4(tmp, { recursive: true, force: true });
|
|
15553
|
-
const r = spawnSync4("git", ["clone", "--depth", "1", "--filter=blob:none", "--sparse", GITHUB_REPO, tmp], { timeout: 120000 });
|
|
15554
|
-
if (r.status === 0) {
|
|
15555
|
-
const sparse = spawnSync4("git", ["-C", tmp, "sparse-checkout", "set", "payloads", "templates"], { timeout: 120000 });
|
|
15556
|
-
if (sparse.status === 0) {
|
|
15557
|
-
if (!dirExists(payloadsPath) || countFiles(payloadsPath) === 0) {
|
|
15558
|
-
copyDir(join21(tmp, "payloads"), payloadsPath);
|
|
15559
|
-
payloadsSynced = true;
|
|
15560
|
-
}
|
|
15561
|
-
if (!dirExists(templatesPath) || countFiles(templatesPath) === 0) {
|
|
15562
|
-
copyDir(join21(tmp, "templates"), templatesPath);
|
|
15563
|
-
templatesSynced = true;
|
|
15564
|
-
}
|
|
15565
|
-
}
|
|
15566
|
-
rmSync4(tmp, { recursive: true, force: true });
|
|
15583
|
+
if (state === "sanitized") {
|
|
15584
|
+
if (context && required.includes(context)) {
|
|
15585
|
+
return { verdict: "safe", state, context, required, reason: `sanitized for '${context}' — matches sink requirement ${required.join("/")}` };
|
|
15567
15586
|
}
|
|
15587
|
+
return { verdict: "vulnerable", state, context, required, reason: `sanitized for '${context}' but this sink requires ${required.join("/")} — WRONG sanitizer (context mismatch)` };
|
|
15568
15588
|
}
|
|
15569
|
-
return {
|
|
15570
|
-
payloads: { path: payloadsPath, files: countFiles(payloadsPath), synced: payloadsSynced },
|
|
15571
|
-
templates: { path: templatesPath, files: countFiles(templatesPath), synced: templatesSynced },
|
|
15572
|
-
data_root: DATA_ROOT2
|
|
15573
|
-
};
|
|
15589
|
+
return { verdict: "unknown", state, context, required, reason: "unhandled state" };
|
|
15574
15590
|
}
|
|
15575
|
-
function
|
|
15576
|
-
|
|
15577
|
-
|
|
15578
|
-
|
|
15579
|
-
|
|
15591
|
+
function sinkTypeOf(line) {
|
|
15592
|
+
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))
|
|
15593
|
+
return "sql_execution";
|
|
15594
|
+
if (/\b(eval|assert)\s*\(/.test(line))
|
|
15595
|
+
return "code_execution";
|
|
15596
|
+
if (/\b(system|exec|shell_exec|passthru|proc_open|popen)\s*\(/.test(line))
|
|
15597
|
+
return "command_execution";
|
|
15598
|
+
if (/\b(unserialize|maybe_unserialize)\s*\(/.test(line))
|
|
15599
|
+
return "deserialization";
|
|
15600
|
+
if (/\b(file_get_contents|file_put_contents|fopen|readfile|include|require|include_once|require_once)\s*\(/.test(line))
|
|
15601
|
+
return "file_operations";
|
|
15602
|
+
if (/header\s*\(\s*['"]Location/i.test(line))
|
|
15603
|
+
return "redirect";
|
|
15604
|
+
return null;
|
|
15580
15605
|
}
|
|
15581
|
-
function
|
|
15582
|
-
|
|
15583
|
-
|
|
15584
|
-
|
|
15585
|
-
|
|
15586
|
-
|
|
15587
|
-
|
|
15588
|
-
|
|
15589
|
-
|
|
15606
|
+
function detectWrongSanitizer(code, file) {
|
|
15607
|
+
const out = [];
|
|
15608
|
+
const ls = code.split(`
|
|
15609
|
+
`);
|
|
15610
|
+
const varSanitizer = new Map;
|
|
15611
|
+
for (const line of ls) {
|
|
15612
|
+
const m = line.match(/\$(\w+)\s*=\s*([a-zA-Z0-9_]+)\s*\(\s*([^)]*)\s*\)\s*;?/);
|
|
15613
|
+
if (!m)
|
|
15614
|
+
continue;
|
|
15615
|
+
const [, varName, fn, arg] = m;
|
|
15616
|
+
if (!SOURCE_TOKENS2.test(arg))
|
|
15617
|
+
continue;
|
|
15618
|
+
if (SANITIZERS2[fn])
|
|
15619
|
+
varSanitizer.set(varName, fn);
|
|
15620
|
+
else if (PSEUDO_SANITIZERS.has(fn))
|
|
15621
|
+
varSanitizer.set(varName, fn);
|
|
15622
|
+
}
|
|
15623
|
+
for (let i = 0;i < ls.length; i++) {
|
|
15624
|
+
const line = ls[i];
|
|
15625
|
+
const sinkType = sinkTypeOf(line);
|
|
15626
|
+
if (!sinkType)
|
|
15627
|
+
continue;
|
|
15628
|
+
const required = SINK_REQUIREMENTS[sinkType] ?? [];
|
|
15629
|
+
for (const [varName, fn] of varSanitizer) {
|
|
15630
|
+
if (!new RegExp(`\\$${varName}\\b`).test(line))
|
|
15631
|
+
continue;
|
|
15632
|
+
const sanitizer = SANITIZERS2[fn];
|
|
15633
|
+
const context = sanitizer?.context ?? null;
|
|
15634
|
+
if (sanitizer && (sanitizer.level === "validated" || context && required.includes(context)))
|
|
15635
|
+
continue;
|
|
15636
|
+
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.`;
|
|
15637
|
+
out.push({
|
|
15638
|
+
file,
|
|
15639
|
+
line: i + 1,
|
|
15640
|
+
type: "wrong_sanitizer",
|
|
15641
|
+
category: "Wrong-context sanitization / pseudo-sanitizer",
|
|
15642
|
+
severity: "high",
|
|
15643
|
+
evidence: line.trim(),
|
|
15644
|
+
detail
|
|
15645
|
+
});
|
|
15646
|
+
}
|
|
15590
15647
|
}
|
|
15648
|
+
const seen = new Set;
|
|
15649
|
+
return out.filter((f) => {
|
|
15650
|
+
const key = `${f.type}:${f.line}`;
|
|
15651
|
+
if (seen.has(key))
|
|
15652
|
+
return false;
|
|
15653
|
+
seen.add(key);
|
|
15654
|
+
return true;
|
|
15655
|
+
});
|
|
15591
15656
|
}
|
|
15592
|
-
var
|
|
15593
|
-
var
|
|
15594
|
-
|
|
15595
|
-
|
|
15657
|
+
var SANITIZERS2, PSEUDO_SANITIZERS, SINK_REQUIREMENTS, SOURCE_TOKENS2;
|
|
15658
|
+
var init_security_state = __esm(() => {
|
|
15659
|
+
SANITIZERS2 = {
|
|
15660
|
+
esc_sql: { context: "sql", level: "sanitized" },
|
|
15661
|
+
addslashes: { context: "sql", level: "sanitized" },
|
|
15662
|
+
mysqli_real_escape_string: { context: "sql", level: "sanitized" },
|
|
15663
|
+
mysql_real_escape_string: { context: "sql", level: "sanitized" },
|
|
15664
|
+
pg_escape_string: { context: "sql", level: "sanitized" },
|
|
15665
|
+
sanitize_text_field: { context: "html", level: "sanitized" },
|
|
15666
|
+
sanitize_textarea_field: { context: "html", level: "sanitized" },
|
|
15667
|
+
esc_html: { context: "html", level: "sanitized" },
|
|
15668
|
+
esc_html__: { context: "html", level: "sanitized" },
|
|
15669
|
+
htmlspecialchars: { context: "html", level: "sanitized" },
|
|
15670
|
+
htmlentities: { context: "html", level: "sanitized" },
|
|
15671
|
+
wp_kses: { context: "html", level: "sanitized" },
|
|
15672
|
+
wp_kses_post: { context: "html", level: "sanitized" },
|
|
15673
|
+
wp_kses_data: { context: "html", level: "sanitized" },
|
|
15674
|
+
strip_tags: { context: "html", level: "sanitized" },
|
|
15675
|
+
esc_attr: { context: "html_attr", level: "sanitized" },
|
|
15676
|
+
esc_url: { context: "url", level: "sanitized" },
|
|
15677
|
+
esc_url_raw: { context: "url", level: "sanitized" },
|
|
15678
|
+
esc_textarea: { context: "html", level: "sanitized" },
|
|
15679
|
+
esc_js: { context: "js", level: "sanitized" },
|
|
15680
|
+
urlencode: { context: "url", level: "sanitized" },
|
|
15681
|
+
rawurlencode: { context: "url", level: "sanitized" },
|
|
15682
|
+
wp_json_encode: { context: "json", level: "sanitized" },
|
|
15683
|
+
json_encode: { context: "json", level: "sanitized" },
|
|
15684
|
+
absint: { context: "int", level: "validated" },
|
|
15685
|
+
intval: { context: "int", level: "validated" },
|
|
15686
|
+
sanitize_key: { context: "key", level: "sanitized" },
|
|
15687
|
+
sanitize_email: { context: "email", level: "validated" },
|
|
15688
|
+
sanitize_file_name: { context: "filename", level: "validated" },
|
|
15689
|
+
sanitize_url: { context: "url", level: "validated" },
|
|
15690
|
+
sanitize_title: { context: "slug", level: "validated" },
|
|
15691
|
+
sanitize_title_with_dashes: { context: "slug", level: "validated" },
|
|
15692
|
+
sanitize_user: { context: "username", level: "validated" },
|
|
15693
|
+
filter_var: { context: "validated", level: "validated" },
|
|
15694
|
+
wp_validate_boolean: { context: "validated", level: "validated" }
|
|
15695
|
+
};
|
|
15696
|
+
PSEUDO_SANITIZERS = new Set([
|
|
15697
|
+
"trim",
|
|
15698
|
+
"ltrim",
|
|
15699
|
+
"rtrim",
|
|
15700
|
+
"strtolower",
|
|
15701
|
+
"strtoupper",
|
|
15702
|
+
"ucfirst",
|
|
15703
|
+
"ucwords",
|
|
15704
|
+
"base64_encode",
|
|
15705
|
+
"base64_decode",
|
|
15706
|
+
"urldecode",
|
|
15707
|
+
"rawurldecode",
|
|
15708
|
+
"stripslashes",
|
|
15709
|
+
"html_entity_decode",
|
|
15710
|
+
"htmlspecialchars_decode",
|
|
15711
|
+
"json_decode",
|
|
15712
|
+
"unserialize",
|
|
15713
|
+
"serialize",
|
|
15714
|
+
"substr",
|
|
15715
|
+
"mb_substr",
|
|
15716
|
+
"explode",
|
|
15717
|
+
"implode",
|
|
15718
|
+
"str_replace",
|
|
15719
|
+
"preg_replace",
|
|
15720
|
+
"nl2br",
|
|
15721
|
+
"strval",
|
|
15722
|
+
"number_format",
|
|
15723
|
+
"sprintf",
|
|
15724
|
+
"md5",
|
|
15725
|
+
"sha1",
|
|
15726
|
+
"hash",
|
|
15727
|
+
"crypt",
|
|
15728
|
+
"chr",
|
|
15729
|
+
"ord",
|
|
15730
|
+
"strrev",
|
|
15731
|
+
"str_repeat",
|
|
15732
|
+
"str_pad"
|
|
15733
|
+
]);
|
|
15734
|
+
SINK_REQUIREMENTS = {
|
|
15735
|
+
sql_execution: ["sql", "sql_param", "int"],
|
|
15736
|
+
code_execution: ["code"],
|
|
15737
|
+
command_execution: ["code"],
|
|
15738
|
+
deserialization: ["code"],
|
|
15739
|
+
file_operations: ["filename"],
|
|
15740
|
+
redirect: ["url"],
|
|
15741
|
+
html_render: ["html"]
|
|
15742
|
+
};
|
|
15743
|
+
SOURCE_TOKENS2 = /\$_(GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|->(input|query|request)\(|\breq\.(query|body|params|headers|cookies)|\bgetParameter\(/;
|
|
15596
15744
|
});
|
|
15597
15745
|
|
|
15598
|
-
// src/
|
|
15599
|
-
|
|
15600
|
-
|
|
15601
|
-
const controller = new AbortController;
|
|
15602
|
-
const t = setTimeout(() => controller.abort(), timeoutMs);
|
|
15603
|
-
const res = await fetch("https://registry.npmjs.org/blitzstrike/latest", { signal: controller.signal });
|
|
15604
|
-
clearTimeout(t);
|
|
15605
|
-
if (!res.ok)
|
|
15606
|
-
return null;
|
|
15607
|
-
const data = await res.json();
|
|
15608
|
-
return data.version ?? null;
|
|
15609
|
-
} catch {
|
|
15610
|
-
return null;
|
|
15611
|
-
}
|
|
15746
|
+
// src/framework-security.ts
|
|
15747
|
+
function frameworkKnowledge(framework = "wordpress") {
|
|
15748
|
+
return FRAMEWORKS[framework.toLowerCase()] ?? null;
|
|
15612
15749
|
}
|
|
15613
|
-
function
|
|
15614
|
-
const
|
|
15615
|
-
const
|
|
15616
|
-
|
|
15617
|
-
|
|
15618
|
-
|
|
15619
|
-
|
|
15620
|
-
|
|
15750
|
+
function listFrameworks2() {
|
|
15751
|
+
const s = new Set;
|
|
15752
|
+
for (const k of Object.keys(FRAMEWORKS))
|
|
15753
|
+
s.add(FRAMEWORKS[k].framework);
|
|
15754
|
+
return [...s];
|
|
15755
|
+
}
|
|
15756
|
+
function lineNoOf(code, pos) {
|
|
15757
|
+
let n = 1;
|
|
15758
|
+
for (let i = 0;i < pos; i++)
|
|
15759
|
+
if (code.charCodeAt(i) === 10)
|
|
15760
|
+
n++;
|
|
15761
|
+
return n;
|
|
15762
|
+
}
|
|
15763
|
+
function functionBody(code, name) {
|
|
15764
|
+
const idx = code.search(new RegExp(`function\\s+${name}\\s*\\(`));
|
|
15765
|
+
if (idx === -1)
|
|
15766
|
+
return null;
|
|
15767
|
+
const open = code.indexOf("{", idx);
|
|
15768
|
+
if (open === -1)
|
|
15769
|
+
return null;
|
|
15770
|
+
let depth = 0;
|
|
15771
|
+
for (let i = open;i < code.length; i++) {
|
|
15772
|
+
if (code[i] === "{")
|
|
15773
|
+
depth++;
|
|
15774
|
+
else if (code[i] === "}") {
|
|
15775
|
+
depth--;
|
|
15776
|
+
if (depth === 0)
|
|
15777
|
+
return code.slice(open, i + 1);
|
|
15778
|
+
}
|
|
15621
15779
|
}
|
|
15622
|
-
return
|
|
15780
|
+
return code.slice(open);
|
|
15623
15781
|
}
|
|
15624
|
-
|
|
15625
|
-
const
|
|
15626
|
-
|
|
15627
|
-
|
|
15628
|
-
|
|
15629
|
-
latest,
|
|
15630
|
-
outdated,
|
|
15631
|
-
update_command: "npm install -g blitzstrike@latest # or: bun add -g blitzstrike@latest",
|
|
15632
|
-
data_sync: syncData()
|
|
15633
|
-
};
|
|
15782
|
+
function handlerName(expr) {
|
|
15783
|
+
const quoted = expr.match(/['"]([A-Za-z_][A-Za-z0-9_]*)['"]/g);
|
|
15784
|
+
if (quoted && quoted.length > 0)
|
|
15785
|
+
return quoted[quoted.length - 1].replace(/['"]/g, "");
|
|
15786
|
+
return null;
|
|
15634
15787
|
}
|
|
15635
|
-
|
|
15636
|
-
|
|
15637
|
-
|
|
15638
|
-
|
|
15639
|
-
|
|
15640
|
-
|
|
15641
|
-
|
|
15642
|
-
|
|
15643
|
-
|
|
15644
|
-
|
|
15645
|
-
|
|
15646
|
-
|
|
15647
|
-
|
|
15648
|
-
const
|
|
15649
|
-
const
|
|
15650
|
-
const
|
|
15651
|
-
|
|
15652
|
-
|
|
15653
|
-
|
|
15654
|
-
|
|
15655
|
-
|
|
15656
|
-
|
|
15657
|
-
checks.push({ name: "CHANGELOG version", ok: false, detail: "CHANGELOG.md not found" });
|
|
15788
|
+
function detectWpAuthz(code, file, out) {
|
|
15789
|
+
const ajaxRe = /add_action\s*\(\s*['"](wp_ajax(?:_nopriv)?_\w+)['"]\s*,\s*(.+?)\s*\)/g;
|
|
15790
|
+
let m;
|
|
15791
|
+
while ((m = ajaxRe.exec(code)) !== null) {
|
|
15792
|
+
const hook = m[1];
|
|
15793
|
+
const handler = handlerName(m[2]);
|
|
15794
|
+
if (!handler)
|
|
15795
|
+
continue;
|
|
15796
|
+
const nopriv = hook.startsWith("wp_ajax_nopriv_");
|
|
15797
|
+
const body = functionBody(code, handler);
|
|
15798
|
+
if (!body)
|
|
15799
|
+
continue;
|
|
15800
|
+
const hasCap = AUTH_RE.test(body) || INDIRECT_AUTH.test(body);
|
|
15801
|
+
const hasNonce = NONCE_RE.test(body);
|
|
15802
|
+
const dangerous = DANGEROUS_OP_RE.test(body);
|
|
15803
|
+
const line = lineNoOf(code, m.index);
|
|
15804
|
+
if (nopriv && !hasCap && dangerous) {
|
|
15805
|
+
out.push({ file, line, type: "missing_authz", category: "Missing capability check (unauthenticated AJAX handler)", severity: "high", evidence: `add_action('${hook}', ...)`, detail: `Unauthenticated AJAX handler '${handler}()' performs a privileged/state-changing operation with no current_user_can()/authorization check — auth bypass / privilege escalation.` });
|
|
15806
|
+
}
|
|
15807
|
+
if (!hasNonce && dangerous) {
|
|
15808
|
+
out.push({ file, line, type: "missing_nonce", category: "Missing nonce verification", severity: "medium", evidence: `add_action('${hook}', ...)`, detail: `AJAX handler '${handler}()' performs a state-changing operation without nonce verification (wp_verify_nonce / check_ajax_referer) — CSRF.` });
|
|
15809
|
+
}
|
|
15658
15810
|
}
|
|
15659
|
-
|
|
15660
|
-
|
|
15661
|
-
|
|
15662
|
-
|
|
15663
|
-
|
|
15811
|
+
const adminRe = /add_action\s*\(\s*['"](admin_post(?:_nopriv)?_\w+)['"]\s*,\s*(.+?)\s*\)/g;
|
|
15812
|
+
while ((m = adminRe.exec(code)) !== null) {
|
|
15813
|
+
const hook = m[1];
|
|
15814
|
+
const handler = handlerName(m[2]);
|
|
15815
|
+
if (!handler)
|
|
15816
|
+
continue;
|
|
15817
|
+
const nopriv = hook.startsWith("admin_post_nopriv_");
|
|
15818
|
+
const body = functionBody(code, handler);
|
|
15819
|
+
if (!body)
|
|
15820
|
+
continue;
|
|
15821
|
+
const hasCap = AUTH_RE.test(body) || INDIRECT_AUTH.test(body);
|
|
15822
|
+
const hasNonce = NONCE_RE.test(body);
|
|
15823
|
+
const dangerous = DANGEROUS_OP_RE.test(body);
|
|
15824
|
+
const line = lineNoOf(code, m.index);
|
|
15825
|
+
if (nopriv && !hasCap && dangerous) {
|
|
15826
|
+
out.push({ file, line, type: "missing_authz", category: "Missing capability check (unauthenticated admin-post handler)", severity: "high", evidence: `add_action('${hook}', ...)`, detail: `Unauthenticated admin-post handler '${handler}()' performs a privileged/state-changing operation with no authorization check — auth bypass / privilege escalation.` });
|
|
15827
|
+
}
|
|
15828
|
+
if (!hasNonce && dangerous) {
|
|
15829
|
+
out.push({ file, line, type: "missing_nonce", category: "Missing nonce verification", severity: "medium", evidence: `add_action('${hook}', ...)`, detail: `admin-post handler '${handler}()' performs a state-changing operation without nonce verification — CSRF.` });
|
|
15830
|
+
}
|
|
15664
15831
|
}
|
|
15665
|
-
|
|
15666
|
-
|
|
15667
|
-
|
|
15668
|
-
|
|
15669
|
-
|
|
15832
|
+
const restRe = /register_rest_route\s*\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]/g;
|
|
15833
|
+
while ((m = restRe.exec(code)) !== null) {
|
|
15834
|
+
const line = lineNoOf(code, m.index);
|
|
15835
|
+
const window2 = code.slice(m.index, m.index + 800);
|
|
15836
|
+
const pc = window2.match(/permission_callback['"]?\s*=>\s*(.+?)(?:,|\s*\n|\])/);
|
|
15837
|
+
if (pc) {
|
|
15838
|
+
const expr = pc[1].trim();
|
|
15839
|
+
if (/__return_true/.test(expr)) {
|
|
15840
|
+
out.push({ file, line, type: "missing_authz", category: "REST route explicitly public (__return_true)", severity: "medium", evidence: `register_rest_route('${m[1]}', '${m[2]}')`, detail: `REST route '${m[1]}${m[2]}' sets permission_callback to __return_true (always allow) — verify the endpoint does not expose sensitive data or perform privileged actions.` });
|
|
15841
|
+
}
|
|
15842
|
+
continue;
|
|
15843
|
+
}
|
|
15844
|
+
out.push({ file, line, type: "missing_authz", category: "REST route missing permission_callback", severity: "high", evidence: `register_rest_route('${m[1]}', '${m[2]}')`, detail: `REST route '${m[1]}${m[2]}' has no permission_callback — the endpoint defaults to public (auth bypass).` });
|
|
15845
|
+
}
|
|
15846
|
+
const formRe = /add_action\s*\(\s*['"](init|admin_init|wp_loaded|template_redirect|parse_request|wp)['"]\s*,\s*(.+?)\s*\)/g;
|
|
15847
|
+
while ((m = formRe.exec(code)) !== null) {
|
|
15848
|
+
const hook = m[1];
|
|
15849
|
+
const handler = handlerName(m[2]);
|
|
15850
|
+
if (!handler)
|
|
15851
|
+
continue;
|
|
15852
|
+
const body = functionBody(code, handler);
|
|
15853
|
+
if (!body)
|
|
15854
|
+
continue;
|
|
15855
|
+
if (!/\$_(GET|POST|REQUEST|COOKIE|SERVER)\b|php:\/\/input|filter_input|file_get_contents\(['"]php:\/\/input/.test(body))
|
|
15856
|
+
continue;
|
|
15857
|
+
const hasCap = AUTH_RE.test(body) || INDIRECT_AUTH.test(body);
|
|
15858
|
+
const hasNonce = NONCE_RE.test(body);
|
|
15859
|
+
const dangerous = DANGEROUS_OP_RE.test(body);
|
|
15860
|
+
const line = lineNoOf(code, m.index);
|
|
15861
|
+
if (!hasNonce && dangerous) {
|
|
15862
|
+
out.push({ file, line, type: "missing_nonce", category: "Missing nonce verification (direct request handler)", severity: "medium", evidence: `add_action('${hook}', ...)`, detail: `Request handler '${handler}()' (on '${hook}') performs a state-changing operation without nonce verification — CSRF.` });
|
|
15863
|
+
}
|
|
15864
|
+
if (!hasCap && dangerous) {
|
|
15865
|
+
out.push({ file, line, type: "missing_authz", category: "Missing capability check (direct request handler)", severity: "high", evidence: `add_action('${hook}', ...)`, detail: `Request handler '${handler}()' (on '${hook}') performs a privileged/state-changing operation with no capability check — auth bypass / privilege escalation.` });
|
|
15866
|
+
}
|
|
15670
15867
|
}
|
|
15671
|
-
checks.push({
|
|
15672
|
-
name: "Lockfile",
|
|
15673
|
-
ok: lockfilePresent(),
|
|
15674
|
-
detail: lockfilePresent() ? "present (bun.lock / package-lock.json)" : "missing",
|
|
15675
|
-
soft: true
|
|
15676
|
-
});
|
|
15677
|
-
return { version: VERSION, checks, consistent: checks.filter((c) => !c.soft).every((c) => c.ok) };
|
|
15678
|
-
}
|
|
15679
|
-
function lockfilePresent() {
|
|
15680
|
-
return existsSync12(join22(ROOT8, "package-lock.json")) || existsSync12(join22(ROOT8, "bun.lock")) || existsSync12(join22(ROOT8, "bun.lockb"));
|
|
15681
15868
|
}
|
|
15682
|
-
|
|
15683
|
-
|
|
15684
|
-
|
|
15685
|
-
|
|
15686
|
-
|
|
15687
|
-
|
|
15688
|
-
|
|
15689
|
-
|
|
15690
|
-
|
|
15691
|
-
|
|
15692
|
-
|
|
15693
|
-
|
|
15694
|
-
|
|
15695
|
-
|
|
15696
|
-
|
|
15697
|
-
|
|
15869
|
+
function detectRouteAuthz(code, file, out) {
|
|
15870
|
+
const ls = code.split(`
|
|
15871
|
+
`);
|
|
15872
|
+
for (const d of ROUTE_DETECTORS) {
|
|
15873
|
+
d.entry.lastIndex = 0;
|
|
15874
|
+
let m;
|
|
15875
|
+
while ((m = d.entry.exec(code)) !== null) {
|
|
15876
|
+
const ln = lineNoOf(code, m.index);
|
|
15877
|
+
const window2 = ls.slice(Math.max(0, ln - d.window), Math.min(ls.length, ln + d.window + 1)).join(`
|
|
15878
|
+
`);
|
|
15879
|
+
if (d.auth.test(window2))
|
|
15880
|
+
continue;
|
|
15881
|
+
out.push({
|
|
15882
|
+
file,
|
|
15883
|
+
line: ln,
|
|
15884
|
+
type: "missing_authz",
|
|
15885
|
+
category: `${d.framework}: endpoint without authorization`,
|
|
15886
|
+
severity: "high",
|
|
15887
|
+
evidence: (ls[ln - 1] ?? "").trim(),
|
|
15888
|
+
detail: `${d.framework} endpoint has no authorization enforcement in the surrounding ${d.window}-line window — auth bypass / privilege escalation.`
|
|
15889
|
+
});
|
|
15890
|
+
}
|
|
15698
15891
|
}
|
|
15699
|
-
const installScriptNames = ["preinstall", "install", "postinstall", "prepare"];
|
|
15700
|
-
const installScripts = installScriptNames.filter((n) => pkg.scripts?.[n]).map((n) => ({ name: n, command: pkg.scripts[n] }));
|
|
15701
|
-
const allDeps = {
|
|
15702
|
-
...pkg.dependencies ?? {},
|
|
15703
|
-
...pkg.devDependencies ?? {},
|
|
15704
|
-
...pkg.peerDependencies ?? {},
|
|
15705
|
-
...pkg.optionalDependencies ?? {}
|
|
15706
|
-
};
|
|
15707
|
-
const gitDependencies = Object.entries(allDeps).filter(([, v]) => /^(git\+|git:|github:|gitlab:|bitbucket:|\w+:\/\/)/.test(v)).map(([name, v]) => `${name}@${v}`);
|
|
15708
|
-
const total = Object.keys(allDeps).length;
|
|
15709
|
-
return {
|
|
15710
|
-
install_scripts: installScripts,
|
|
15711
|
-
git_dependencies: gitDependencies,
|
|
15712
|
-
total_dependencies: total,
|
|
15713
|
-
safe: installScripts.length === 0 && gitDependencies.length === 0
|
|
15714
|
-
};
|
|
15715
15892
|
}
|
|
15716
|
-
|
|
15717
|
-
|
|
15718
|
-
|
|
15893
|
+
function detectMissingAuthz(code, file) {
|
|
15894
|
+
const out = [];
|
|
15895
|
+
detectWpAuthz(code, file, out);
|
|
15896
|
+
detectRouteAuthz(code, file, out);
|
|
15897
|
+
const seen = new Set;
|
|
15898
|
+
return out.filter((f) => {
|
|
15899
|
+
const key = `${f.type}:${f.line}`;
|
|
15900
|
+
if (seen.has(key))
|
|
15901
|
+
return false;
|
|
15902
|
+
seen.add(key);
|
|
15903
|
+
return true;
|
|
15904
|
+
});
|
|
15905
|
+
}
|
|
15906
|
+
var FRAMEWORKS, INDIRECT_AUTH, AUTH_RE, NONCE_RE, DANGEROUS_OP_RE, ROUTE_DETECTORS;
|
|
15907
|
+
var init_framework_security = __esm(() => {
|
|
15908
|
+
FRAMEWORKS = {
|
|
15909
|
+
wordpress: {
|
|
15910
|
+
framework: "wordpress",
|
|
15911
|
+
auth_primitives: ["current_user_can", "current_user_can_edit_post", "is_user_logged_in", "authorize"],
|
|
15912
|
+
nonce_primitives: ["wp_verify_nonce", "check_ajax_referer", "check_admin_referer", "verify_nonce"],
|
|
15913
|
+
entry_point_hooks: ["wp_ajax_", "wp_ajax_nopriv_", "admin_post_", "admin_post_nopriv_", "register_rest_route", "add_shortcode", "init", "rest_api_init"],
|
|
15914
|
+
sanitizers: ["sanitize_text_field", "esc_sql", "esc_html", "esc_attr", "esc_url", "wp_kses", "wp_kses_post", "absint", "sanitize_email", "sanitize_file_name"]
|
|
15915
|
+
},
|
|
15916
|
+
wp: {
|
|
15917
|
+
framework: "wordpress",
|
|
15918
|
+
auth_primitives: ["current_user_can", "is_user_logged_in", "authorize"],
|
|
15919
|
+
nonce_primitives: ["wp_verify_nonce", "check_ajax_referer", "check_admin_referer"],
|
|
15920
|
+
entry_point_hooks: ["wp_ajax_", "wp_ajax_nopriv_", "register_rest_route"],
|
|
15921
|
+
sanitizers: ["sanitize_text_field", "esc_sql", "esc_html", "absint"]
|
|
15922
|
+
},
|
|
15923
|
+
laravel: {
|
|
15924
|
+
framework: "laravel",
|
|
15925
|
+
auth_primitives: ["auth", "authorize", "Gate::", "can:", "->middleware('auth'", "->can(", "$this->authorize", "@can", "@cannot", "policy"],
|
|
15926
|
+
nonce_primitives: ["@csrf", "csrf_field", "VerifyCsrfToken"],
|
|
15927
|
+
entry_point_hooks: ["Route::get", "Route::post", "Route::put", "Route::delete", "Route::resource", "Route::match"],
|
|
15928
|
+
sanitizers: ["e(", "htmlspecialchars", "clean(", "strip_tags"]
|
|
15929
|
+
},
|
|
15930
|
+
django: {
|
|
15931
|
+
framework: "django",
|
|
15932
|
+
auth_primitives: ["login_required", "permission_required", "user_passes_test", "LoginRequiredMixin", "PermissionRequiredMixin", "permission_classes", "IsAuthenticated", "has_permission"],
|
|
15933
|
+
nonce_primitives: ["csrf_protect", "CsrfViewMiddleware", "@csrf"],
|
|
15934
|
+
entry_point_hooks: ["def ", "@api_view", "path(", "url(", "re_path("],
|
|
15935
|
+
sanitizers: ["escape(", "mark_safe", "html.escape"]
|
|
15936
|
+
},
|
|
15937
|
+
flask: {
|
|
15938
|
+
framework: "flask",
|
|
15939
|
+
auth_primitives: ["login_required", "@login_required", "current_user.is_authenticated", "permission_required"],
|
|
15940
|
+
nonce_primitives: ["CSRFProtect", "csrf_token"],
|
|
15941
|
+
entry_point_hooks: ["@app.route", "@blueprint.route", "@bp.route"],
|
|
15942
|
+
sanitizers: ["escape(", "Markup", "html.escape"]
|
|
15943
|
+
},
|
|
15944
|
+
express: {
|
|
15945
|
+
framework: "express",
|
|
15946
|
+
auth_primitives: ["requireAuth", "authenticate", "passport.authenticate", "req.user", "isAuthenticated", "ensureAuthenticated", "requireLogin", "authorize", "authMiddleware"],
|
|
15947
|
+
nonce_primitives: ["csurf", "csrfProtection", "helmet"],
|
|
15948
|
+
entry_point_hooks: ["app.get", "app.post", "router.get", "router.post"],
|
|
15949
|
+
sanitizers: ["escapeHtml", "encodeURIComponent", "sanitize", "xss"]
|
|
15950
|
+
},
|
|
15951
|
+
spring: {
|
|
15952
|
+
framework: "spring",
|
|
15953
|
+
auth_primitives: ["@PreAuthorize", "@Secured", "@RolesAllowed", "hasRole", "hasAuthority", "isAuthenticated", "SecurityContextHolder"],
|
|
15954
|
+
nonce_primitives: ["csrf", "CsrfToken"],
|
|
15955
|
+
entry_point_hooks: ["@GetMapping", "@PostMapping", "@RequestMapping", "@Controller"],
|
|
15956
|
+
sanitizers: ["HtmlUtils.htmlEscape", "escapeHtml", "StringEscapeUtils"]
|
|
15957
|
+
},
|
|
15958
|
+
symfony: {
|
|
15959
|
+
framework: "symfony",
|
|
15960
|
+
auth_primitives: ["is_granted", "denyAccessUnlessGranted", "->isGranted(", "IsGranted", "Voter"],
|
|
15961
|
+
nonce_primitives: ["isCsrfTokenValid", "csrf_token"],
|
|
15962
|
+
entry_point_hooks: ["#[Route", "@Route", "#[Get", "#[Post"],
|
|
15963
|
+
sanitizers: ["e(", "htmlspecialchars", "escape("]
|
|
15964
|
+
},
|
|
15965
|
+
aspnet: {
|
|
15966
|
+
framework: "aspnet",
|
|
15967
|
+
auth_primitives: ["[Authorize", "[AllowAnonymous", "User.Identity", "IsInRole", "AuthorizeAsync", "IAuthorizationService"],
|
|
15968
|
+
nonce_primitives: ["ValidateAntiForgeryToken", "Antiforgery"],
|
|
15969
|
+
entry_point_hooks: ["[HttpGet", "[HttpPost", "[Route("],
|
|
15970
|
+
sanitizers: ["HtmlEncoder", "Html.Raw", "UrlEncoder"]
|
|
15971
|
+
},
|
|
15972
|
+
rails: {
|
|
15973
|
+
framework: "rails",
|
|
15974
|
+
auth_primitives: ["authenticate_user!", "authorize", "before_action :authenticate", "before_action :authorize", "current_user"],
|
|
15975
|
+
nonce_primitives: ["protect_from_forgery", "form_authenticity_token"],
|
|
15976
|
+
entry_point_hooks: ["def ", "before_action"],
|
|
15977
|
+
sanitizers: ["h(", "html_escape", "sanitize"]
|
|
15978
|
+
}
|
|
15979
|
+
};
|
|
15980
|
+
INDIRECT_AUTH = /\b[a-zA-Z0-9_]*(auth|permission|cap|nonce|can_|check|verify|allow)[a-zA-Z0-9_]*\s*\(/;
|
|
15981
|
+
AUTH_RE = /current_user_can|is_user_logged_in|is_super_admin|current_user_can_edit_post|authorize/;
|
|
15982
|
+
NONCE_RE = /wp_verify_nonce|check_ajax_referer|check_admin_referer|verify_nonce/;
|
|
15983
|
+
DANGEROUS_OP_RE = /\b(update_option|add_option|delete_option|update_site_option|delete_site_option|update_post_meta|add_post_meta|delete_post_meta|update_user_meta|add_user_meta|delete_user_meta|update_metadata|add_metadata|delete_metadata|set_transient|delete_transient|wp_insert_post|wp_update_post|wp_delete_post|wp_trash_post|wp_untrash_post|wp_insert_comment|wp_update_comment|wp_delete_comment|wp_insert_user|wp_update_user|wp_delete_user|wp_create_user|wp_set_current_user|wp_set_auth_cookie|set_user_role|set_role|add_role|remove_role|add_cap|remove_cap|wp_mail|wp_new_user_notification|wp_handle_upload|move_uploaded_file|wp_upload_bits|wp_set_object_terms|wp_set_post_terms|wp_schedule_event|wp_clear_scheduled_hook|wp_redirect|file_put_contents|unlink|rename|wpdb->query|wpdb->insert|wpdb->update|wpdb->delete|wpdb->replace)\s*\(/;
|
|
15984
|
+
ROUTE_DETECTORS = [
|
|
15985
|
+
{ framework: "laravel", entry: /Route::(?:get|post|put|patch|delete|any|match|resource)\s*\(\s*['"][^'"]+['"]/g, auth: /->middleware\s*\(\s*['"]auth|middleware\s*\(\s*\[[^\]]*['"]auth|->can\s*\(|\$this->authorize|Gate::|authorize\s*\(|@can\b/, window: 2 },
|
|
15986
|
+
{ framework: "django", entry: /@api_view\s*\(|def\s+\w+\s*\(\s*request\s*[,)]/g, auth: /login_required|permission_required|user_passes_test|LoginRequiredMixin|PermissionRequiredMixin|permission_classes|IsAuthenticated|has_permission/, window: 3 },
|
|
15987
|
+
{ framework: "flask", entry: /@(?:app|bp|blueprint)\.route\s*\(/g, auth: /login_required|current_user\.is_authenticated|permission_required/, window: 3 },
|
|
15988
|
+
{ framework: "express", entry: /(?:app|router)\.(?:get|post|put|delete|patch|use)\s*\(\s*['"][^'"]+['"]/g, auth: /requireAuth|authenticate|passport\.authenticate|req\.user|isAuthenticated|ensureAuthenticated|requireLogin|\.authorize|authMiddleware/, window: 2 },
|
|
15989
|
+
{ framework: "spring", entry: /@(?:Get|Post|Put|Delete|Patch|Request)Mapping\s*\(/g, auth: /@PreAuthorize|@Secured|@RolesAllowed|hasRole|hasAuthority|isAuthenticated|SecurityContextHolder/, window: 2 },
|
|
15990
|
+
{ framework: "symfony", entry: /#\[(?:Route|Get|Post|Put|Delete|Patch)\b|@Route\s*\(/g, auth: /is_granted|denyAccessUnlessGranted|->isGranted\s*\(|IsGranted|Voter/, window: 2 },
|
|
15991
|
+
{ framework: "aspnet", entry: /\[Http(?:Get|Post|Put|Delete|Patch)\b/g, auth: /\[Authorize|User\.Identity|IsInRole|AuthorizeAsync|\[AllowAnonymous|IAuthorizationService/, window: 2 },
|
|
15992
|
+
{ framework: "rails", entry: /class\s+\w+\s*<\s*ApplicationController/g, auth: /before_action\s+:authenticate|before_action\s+:authorize|authenticate_user!|authorize\s+|current_user|authenticate/, window: 6 }
|
|
15993
|
+
];
|
|
15719
15994
|
});
|
|
15720
15995
|
|
|
15721
|
-
// src/
|
|
15722
|
-
|
|
15723
|
-
|
|
15724
|
-
|
|
15725
|
-
import { fileURLToPath as fileURLToPath14 } from "node:url";
|
|
15726
|
-
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
15727
|
-
function which(cmd) {
|
|
15728
|
-
return spawnSync5("sh", ["-c", 'command -v "$1" >/dev/null 2>&1', "sh", cmd]).status === 0;
|
|
15996
|
+
// src/complex-bugs.ts
|
|
15997
|
+
function lines2(code) {
|
|
15998
|
+
return code.split(`
|
|
15999
|
+
`);
|
|
15729
16000
|
}
|
|
15730
|
-
function
|
|
15731
|
-
|
|
15732
|
-
|
|
15733
|
-
|
|
16001
|
+
function lineNo2(code, pos) {
|
|
16002
|
+
let n = 1;
|
|
16003
|
+
for (let i = 0;i < pos; i++)
|
|
16004
|
+
if (code.charCodeAt(i) === 10)
|
|
16005
|
+
n++;
|
|
16006
|
+
return n;
|
|
15734
16007
|
}
|
|
15735
|
-
|
|
15736
|
-
|
|
15737
|
-
|
|
15738
|
-
|
|
15739
|
-
|
|
15740
|
-
|
|
15741
|
-
|
|
16008
|
+
function stripCommentsOnly(code) {
|
|
16009
|
+
const out = code.split("");
|
|
16010
|
+
const n = code.length;
|
|
16011
|
+
let i = 0;
|
|
16012
|
+
while (i < n) {
|
|
16013
|
+
const c = code[i];
|
|
16014
|
+
const nc = code[i + 1];
|
|
16015
|
+
if (c === "'" || c === '"') {
|
|
16016
|
+
const q = c;
|
|
16017
|
+
i++;
|
|
16018
|
+
while (i < n && code[i] !== q) {
|
|
16019
|
+
if (code[i] === "\\")
|
|
16020
|
+
i++;
|
|
16021
|
+
i++;
|
|
16022
|
+
}
|
|
16023
|
+
i++;
|
|
16024
|
+
continue;
|
|
16025
|
+
}
|
|
16026
|
+
if (c === "/" && nc === "/" || c === "#") {
|
|
16027
|
+
while (i < n && code[i] !== `
|
|
16028
|
+
`) {
|
|
16029
|
+
out[i] = " ";
|
|
16030
|
+
i++;
|
|
16031
|
+
}
|
|
16032
|
+
continue;
|
|
16033
|
+
}
|
|
16034
|
+
if (c === "/" && nc === "*") {
|
|
16035
|
+
out[i] = " ";
|
|
16036
|
+
out[i + 1] = " ";
|
|
16037
|
+
i += 2;
|
|
16038
|
+
while (i < n && !(code[i] === "*" && code[i + 1] === "/")) {
|
|
16039
|
+
if (code[i] !== `
|
|
16040
|
+
`)
|
|
16041
|
+
out[i] = " ";
|
|
16042
|
+
i++;
|
|
16043
|
+
}
|
|
16044
|
+
if (i < n) {
|
|
16045
|
+
out[i] = " ";
|
|
16046
|
+
if (i + 1 < n)
|
|
16047
|
+
out[i + 1] = " ";
|
|
16048
|
+
i += 2;
|
|
16049
|
+
}
|
|
16050
|
+
continue;
|
|
16051
|
+
}
|
|
16052
|
+
i++;
|
|
16053
|
+
}
|
|
16054
|
+
return out.join("");
|
|
16055
|
+
}
|
|
16056
|
+
function detectDeserialization(code, file, out) {
|
|
16057
|
+
if (!MAGIC_METHODS.test(code))
|
|
16058
|
+
return;
|
|
16059
|
+
const ls = lines2(code);
|
|
16060
|
+
const re = /\b(unserialize|maybe_unserialize)\s*\(/g;
|
|
16061
|
+
let m;
|
|
16062
|
+
while ((m = re.exec(code)) !== null) {
|
|
16063
|
+
const ln = lineNo2(code, m.index);
|
|
16064
|
+
const l = ls[ln - 1] ?? "";
|
|
16065
|
+
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
16066
|
+
`);
|
|
16067
|
+
if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
|
|
16068
|
+
continue;
|
|
16069
|
+
out.push({
|
|
16070
|
+
file,
|
|
16071
|
+
line: ln,
|
|
16072
|
+
type: "deserialization",
|
|
16073
|
+
category: "Deserialization → POP gadget chain",
|
|
16074
|
+
severity: "high",
|
|
16075
|
+
evidence: l.trim(),
|
|
16076
|
+
detail: `\`${m[1]}()\` of attacker-controlled data AND a magic method is present — object injection can pivot to RCE via the gadget chain.`
|
|
16077
|
+
});
|
|
16078
|
+
}
|
|
16079
|
+
}
|
|
16080
|
+
function detectTypeJuggling(code, file, out) {
|
|
16081
|
+
const ls = lines2(code);
|
|
16082
|
+
const re = /(?<![=!<>])(==|!=)(?!=)/g;
|
|
16083
|
+
let m;
|
|
16084
|
+
while ((m = re.exec(code)) !== null) {
|
|
16085
|
+
const ln = lineNo2(code, m.index);
|
|
16086
|
+
const l = ls[ln - 1] ?? "";
|
|
16087
|
+
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
16088
|
+
`);
|
|
16089
|
+
const hasSource = SOURCE_TOKENS3.test(l) || SOURCE_TOKENS3.test(scope);
|
|
16090
|
+
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);
|
|
16091
|
+
if (!hasSource || !hasHash)
|
|
16092
|
+
continue;
|
|
16093
|
+
out.push({
|
|
16094
|
+
file,
|
|
16095
|
+
line: ln,
|
|
16096
|
+
type: "type_juggling",
|
|
16097
|
+
category: "Type juggling (loose comparison)",
|
|
16098
|
+
severity: "medium",
|
|
16099
|
+
evidence: l.trim(),
|
|
16100
|
+
detail: 'Loose comparison of user input against a hash/secret — PHP type juggling (`0e…`, `"str" == 0`) may bypass the check. Use strict `===`.'
|
|
16101
|
+
});
|
|
16102
|
+
}
|
|
16103
|
+
}
|
|
16104
|
+
function detectMassAssignment(code, file, out) {
|
|
16105
|
+
const ls = lines2(code);
|
|
16106
|
+
const extractRe = /\bextract\s*\(\s*[^)]*(REQUEST|POST|GET|COOKIE|FILES|\$_(REQUEST|POST|GET|COOKIE|FILES))/g;
|
|
16107
|
+
let m;
|
|
16108
|
+
while ((m = extractRe.exec(code)) !== null) {
|
|
16109
|
+
const ln = lineNo2(code, m.index);
|
|
16110
|
+
const l = ls[ln - 1] ?? "";
|
|
16111
|
+
if (/EXTR_SKIP|EXTR_PREFIX_ALL/.test(l))
|
|
16112
|
+
continue;
|
|
16113
|
+
out.push({
|
|
16114
|
+
file,
|
|
16115
|
+
line: ln,
|
|
16116
|
+
type: "mass_assignment",
|
|
16117
|
+
category: "Mass assignment (variable injection)",
|
|
16118
|
+
severity: "high",
|
|
16119
|
+
evidence: l.trim(),
|
|
16120
|
+
detail: "`extract()` of request data without EXTR_SKIP — attacker can overwrite arbitrary variables."
|
|
16121
|
+
});
|
|
16122
|
+
}
|
|
16123
|
+
const parseRe = /\bparse_str\s*\(/g;
|
|
16124
|
+
while ((m = parseRe.exec(code)) !== null) {
|
|
16125
|
+
const ln = lineNo2(code, m.index);
|
|
16126
|
+
const l = ls[ln - 1] ?? "";
|
|
16127
|
+
const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
|
|
16128
|
+
`);
|
|
16129
|
+
if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
|
|
16130
|
+
continue;
|
|
16131
|
+
out.push({
|
|
16132
|
+
file,
|
|
16133
|
+
line: ln,
|
|
16134
|
+
type: "mass_assignment",
|
|
16135
|
+
category: "Mass assignment (variable injection)",
|
|
16136
|
+
severity: "medium",
|
|
16137
|
+
evidence: l.trim(),
|
|
16138
|
+
detail: "`parse_str()` of user input into the symbol table — variable injection."
|
|
16139
|
+
});
|
|
16140
|
+
}
|
|
16141
|
+
}
|
|
16142
|
+
function detectComplexBugs(code, file) {
|
|
16143
|
+
const out = [];
|
|
16144
|
+
detectDeserialization(code, file, out);
|
|
16145
|
+
detectTypeJuggling(code, file, out);
|
|
16146
|
+
detectMassAssignment(code, file, out);
|
|
16147
|
+
detectPrototypePollution(code, file, out);
|
|
16148
|
+
detectCrlfInjection(code, file, out);
|
|
16149
|
+
detectPathConfusion(code, file, out);
|
|
16150
|
+
detectSsrF(code, file, out);
|
|
16151
|
+
detectXxe(code, file, out);
|
|
16152
|
+
detectSsti(code, file, out);
|
|
16153
|
+
detectFileUploadRce(code, file, out);
|
|
16154
|
+
detectUnrestrictedUpload(code, file, out);
|
|
16155
|
+
detectSqlInjection(code, file, out);
|
|
16156
|
+
detectHardcodedSecret(code, file, out);
|
|
16157
|
+
detectLlmInjection(code, file, out);
|
|
16158
|
+
detectGraphqlExposure(code, file, out);
|
|
16159
|
+
detectOauthMisconfig(code, file, out);
|
|
16160
|
+
detectGrpcReflection(code, file, out);
|
|
16161
|
+
out.push(...detectWrongSanitizer(code, file));
|
|
16162
|
+
out.push(...detectMissingAuthz(code, file));
|
|
16163
|
+
const seen = new Set;
|
|
16164
|
+
return out.filter((f) => {
|
|
16165
|
+
const key = `${f.type}:${f.line}`;
|
|
16166
|
+
if (seen.has(key))
|
|
16167
|
+
return false;
|
|
16168
|
+
seen.add(key);
|
|
16169
|
+
return true;
|
|
16170
|
+
});
|
|
16171
|
+
}
|
|
16172
|
+
function detectPrototypePollution(code, file, out) {
|
|
16173
|
+
const ls = lines2(code);
|
|
16174
|
+
const mergeRe = /\b(Object\.assign|_\.merge|_\.defaultsDeep|\.extend|\.merge|\.defaultsDeep|mergeDeep|deepMerge|_.set)\s*\(/g;
|
|
16175
|
+
let m;
|
|
16176
|
+
while ((m = mergeRe.exec(code)) !== null) {
|
|
16177
|
+
const ln = lineNo2(code, m.index);
|
|
16178
|
+
const l = ls[ln - 1] ?? "";
|
|
16179
|
+
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
16180
|
+
`);
|
|
16181
|
+
if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
|
|
16182
|
+
continue;
|
|
16183
|
+
out.push({
|
|
16184
|
+
file,
|
|
16185
|
+
line: ln,
|
|
16186
|
+
type: "prototype_pollution",
|
|
16187
|
+
category: "Prototype pollution (unsafe merge)",
|
|
16188
|
+
severity: "high",
|
|
16189
|
+
evidence: l.trim(),
|
|
16190
|
+
detail: "Merge/extend of request data into an object without a `__proto__`/`constructor.prototype` guard — attacker-controlled keys can pollute `Object.prototype` and pivot to RCE or property injection."
|
|
16191
|
+
});
|
|
16192
|
+
}
|
|
16193
|
+
}
|
|
16194
|
+
function detectCrlfInjection(code, file, out) {
|
|
16195
|
+
const ls = lines2(code);
|
|
16196
|
+
const headerRe = /\b(header|setHeader|addHeader|writeHead|appendHeader)\s*\(/g;
|
|
16197
|
+
let m;
|
|
16198
|
+
while ((m = headerRe.exec(code)) !== null) {
|
|
16199
|
+
const ln = lineNo2(code, m.index);
|
|
16200
|
+
const l = ls[ln - 1] ?? "";
|
|
16201
|
+
const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
|
|
16202
|
+
`);
|
|
16203
|
+
if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
|
|
16204
|
+
continue;
|
|
16205
|
+
out.push({
|
|
16206
|
+
file,
|
|
16207
|
+
line: ln,
|
|
16208
|
+
type: "crlf_injection",
|
|
16209
|
+
category: "CRLF / header injection",
|
|
16210
|
+
severity: "medium",
|
|
16211
|
+
evidence: l.trim(),
|
|
16212
|
+
detail: "User input reaches an HTTP header — CRLF (`%0d%0a`) can split the response, inject headers, or poison cookies. Strip newlines from header values."
|
|
16213
|
+
});
|
|
16214
|
+
}
|
|
16215
|
+
}
|
|
16216
|
+
function detectPathConfusion(code, file, out) {
|
|
16217
|
+
const ls = lines2(code);
|
|
16218
|
+
const fileRe = /\b(file_get_contents|file_put_contents|fopen|readFileSync|readFile|open|include|require|new\s+File|Files\.read|FileInputStream|FileOutputStream)\s*\(/g;
|
|
16219
|
+
let m;
|
|
16220
|
+
while ((m = fileRe.exec(code)) !== null) {
|
|
16221
|
+
const ln = lineNo2(code, m.index);
|
|
16222
|
+
const l = ls[ln - 1] ?? "";
|
|
16223
|
+
const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
|
|
16224
|
+
`);
|
|
16225
|
+
if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
|
|
16226
|
+
continue;
|
|
16227
|
+
if (/\b(basename|realpath|canonicalPath|getCanonicalPath|normalize|resolve|toRealPath)\s*\(/i.test(scope + `
|
|
16228
|
+
` + l))
|
|
16229
|
+
continue;
|
|
16230
|
+
if (/\$[A-Za-z_]\w*\s*\[\s*\$[A-Za-z_]\w*\s*\]/.test(scope + `
|
|
16231
|
+
` + l))
|
|
16232
|
+
continue;
|
|
16233
|
+
if (/["'][^"']*:\/\/[^"']*["']/.test(scope + `
|
|
16234
|
+
` + l))
|
|
16235
|
+
continue;
|
|
16236
|
+
out.push({
|
|
16237
|
+
file,
|
|
16238
|
+
line: ln,
|
|
16239
|
+
type: "path_confusion",
|
|
16240
|
+
category: "Path confusion (unsanitized file path)",
|
|
16241
|
+
severity: "medium",
|
|
16242
|
+
evidence: l.trim(),
|
|
16243
|
+
detail: "User input reaches a file path with no canonicalization (`basename`/`realpath`/`normalize`) — traversal, `..%2f`, backslash and null-byte variants can confuse a naive `../` filter."
|
|
16244
|
+
});
|
|
16245
|
+
}
|
|
16246
|
+
}
|
|
16247
|
+
function detectSsrF(code, file, out) {
|
|
16248
|
+
const ls = lines2(code);
|
|
16249
|
+
const re = /\b(file_get_contents|fopen|file|readfile|curl_setopt|curl_init|fetch|axios|requests\.(get|post|put)|urllib\.request|http_get|wp_remote_(get|post)|new\s+URL\s*\(|got\(|http\.(get|request))\s*\(/g;
|
|
16250
|
+
let m;
|
|
16251
|
+
while ((m = re.exec(code)) !== null) {
|
|
16252
|
+
const ln = lineNo2(code, m.index);
|
|
16253
|
+
const l = ls[ln - 1] ?? "";
|
|
16254
|
+
const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
|
|
16255
|
+
`);
|
|
16256
|
+
if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
|
|
16257
|
+
continue;
|
|
16258
|
+
if (/(allowlist|whitelist|in_array|preg_match|parse_url|startsWith\(["']https?|endsWith\(["']|basename|realpath|normalize)/i.test(scope + `
|
|
16259
|
+
` + l))
|
|
16260
|
+
continue;
|
|
16261
|
+
if (/["'][^"']*:\/\/[^"']*["']/.test(scope + `
|
|
16262
|
+
` + l))
|
|
16263
|
+
continue;
|
|
16264
|
+
out.push({
|
|
16265
|
+
file,
|
|
16266
|
+
line: ln,
|
|
16267
|
+
type: "ssrf",
|
|
16268
|
+
category: "SSRF (user-controlled URL)",
|
|
16269
|
+
severity: "high",
|
|
16270
|
+
evidence: l.trim(),
|
|
16271
|
+
detail: "User input reaches a URL-accepting sink with no host allowlist — SSRF (cloud metadata, internal port scan, IAM credential theft)."
|
|
16272
|
+
});
|
|
16273
|
+
}
|
|
16274
|
+
}
|
|
16275
|
+
function detectXxe(code, file, out) {
|
|
16276
|
+
const ls = lines2(code);
|
|
16277
|
+
const re = /\b(simplexml_load_string|simplexml_load_file|new\s+SimpleXMLElement|->loadXML\s*\(|->load\s*\(|XMLReader\s*->\s*XML\s*\(|libxml_parse)\s*\(?/g;
|
|
16278
|
+
let m;
|
|
16279
|
+
while ((m = re.exec(code)) !== null) {
|
|
16280
|
+
const ln = lineNo2(code, m.index);
|
|
16281
|
+
const l = ls[ln - 1] ?? "";
|
|
16282
|
+
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
16283
|
+
`);
|
|
16284
|
+
if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
|
|
16285
|
+
continue;
|
|
16286
|
+
if (/libxml_disable_entity_loader\s*\(\s*true|LIBXML_NONET/.test(scope + `
|
|
16287
|
+
` + l))
|
|
16288
|
+
continue;
|
|
16289
|
+
out.push({
|
|
16290
|
+
file,
|
|
16291
|
+
line: ln,
|
|
16292
|
+
type: "xxe",
|
|
16293
|
+
category: "XXE (unprotected XML parse)",
|
|
16294
|
+
severity: "high",
|
|
16295
|
+
evidence: l.trim(),
|
|
16296
|
+
detail: "User-controlled XML parsed without disabling external entities — XXE (file read, SSRF, billion-laughs DoS)."
|
|
16297
|
+
});
|
|
16298
|
+
}
|
|
16299
|
+
}
|
|
16300
|
+
function detectSsti(code, file, out) {
|
|
16301
|
+
const ls = lines2(code);
|
|
16302
|
+
const re = /\b(render_template_string|Template\s*\(|res\.render\s*\(|ejs\.(render|compile)\s*\(|_\.template\s*\(|render_string|twig\.render|Jinja2\(|jinja2\.Template)\s*\(?/g;
|
|
16303
|
+
let m;
|
|
16304
|
+
while ((m = re.exec(code)) !== null) {
|
|
16305
|
+
const ln = lineNo2(code, m.index);
|
|
16306
|
+
const l = ls[ln - 1] ?? "";
|
|
16307
|
+
const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
|
|
16308
|
+
`);
|
|
16309
|
+
if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
|
|
16310
|
+
continue;
|
|
16311
|
+
out.push({
|
|
16312
|
+
file,
|
|
16313
|
+
line: ln,
|
|
16314
|
+
type: "ssti",
|
|
16315
|
+
category: "SSTI (template render of user input)",
|
|
16316
|
+
severity: "high",
|
|
16317
|
+
evidence: l.trim(),
|
|
16318
|
+
detail: "User input reaches a template render with no sandbox — SSTI (`{{7*7}}`, `${7*7}`, `#{7*7}`) → RCE."
|
|
16319
|
+
});
|
|
16320
|
+
}
|
|
16321
|
+
}
|
|
16322
|
+
function detectFileUploadRce(code, file, out) {
|
|
16323
|
+
code = stripCommentsOnly(code);
|
|
16324
|
+
const ls = lines2(code);
|
|
16325
|
+
const re = /\b(move_uploaded_file|wp_upload_bits|->move\s*\(|storeAs\s*\(|->storeAs\s*\(|putFile\s*\()\s*\(/g;
|
|
16326
|
+
let m;
|
|
16327
|
+
while ((m = re.exec(code)) !== null) {
|
|
16328
|
+
const ln = lineNo2(code, m.index);
|
|
16329
|
+
const l = ls[ln - 1] ?? "";
|
|
16330
|
+
const scope = ls.slice(Math.max(0, ln - 30), ln + 3).join(`
|
|
16331
|
+
`);
|
|
16332
|
+
if (!/\$_FILES\b|tmp_name|getUploadedFile|uploadedFile|\['name'\]/i.test(scope))
|
|
16333
|
+
continue;
|
|
16334
|
+
if (/(wp_check_filetype|PATHINFO_EXTENSION|wp_get_mime_types|allowed_?(types|extensions|exts)|mime_?(types|extensions)|in_array\s*\([^)]*\$(?:allowed|whitelist|ext)|wp_handle_upload\s*\(\s*[^)]*test_form\s*=>\s*true)/i.test(scope))
|
|
16335
|
+
continue;
|
|
16336
|
+
out.push({
|
|
16337
|
+
file,
|
|
16338
|
+
line: ln,
|
|
16339
|
+
type: "file_upload",
|
|
16340
|
+
category: "File upload → RCE (unvalidated extension)",
|
|
16341
|
+
severity: "high",
|
|
16342
|
+
evidence: l.trim(),
|
|
16343
|
+
detail: "An uploaded file is written with an attacker-controlled name and no visible extension/filetype validation — upload a .php/.phtml shell → RCE."
|
|
16344
|
+
});
|
|
16345
|
+
}
|
|
16346
|
+
}
|
|
16347
|
+
function detectUnrestrictedUpload(code, file, out) {
|
|
16348
|
+
const ls = lines2(code);
|
|
16349
|
+
const re = /['"]uploadAllow['"]\s*=>\s*(?:array\s*\(\s*)?['"]all['"]/g;
|
|
16350
|
+
let m;
|
|
16351
|
+
while ((m = re.exec(code)) !== null) {
|
|
16352
|
+
const ln = lineNo2(code, m.index);
|
|
16353
|
+
const l = ls[ln - 1] ?? "";
|
|
16354
|
+
const scope = ls.slice(Math.max(0, ln - 4), ln + 4).join(`
|
|
16355
|
+
`);
|
|
16356
|
+
const denyFirst = /['"]uploadOrder['"]\s*=>\s*array\s*\(\s*['"]deny['"]/.test(scope);
|
|
16357
|
+
out.push({
|
|
16358
|
+
file,
|
|
16359
|
+
line: ln,
|
|
16360
|
+
type: "file_upload",
|
|
16361
|
+
category: "Unrestricted upload config (uploadAllow=all)",
|
|
16362
|
+
severity: denyFirst ? "high" : "medium",
|
|
16363
|
+
evidence: l.trim(),
|
|
16364
|
+
detail: denyFirst ? "File-manager/connector config sets uploadAllow='all' with uploadOrder deny→allow — every mimetype (including .php) is uploadable → RCE." : "File-manager/connector config sets uploadAllow='all' — any file type can be uploaded; verify the upload path is not web-reachable or that a server-side extension whitelist exists."
|
|
16365
|
+
});
|
|
16366
|
+
}
|
|
16367
|
+
}
|
|
16368
|
+
function detectSqlInjection(code, file, out) {
|
|
16369
|
+
const ls = lines2(code);
|
|
16370
|
+
const sinkRe = /\$wpdb->(get_results|get_col|get_var|get_row|query)\s*\(|->query\s*\(|DB::unprepared\s*\(|->whereRaw\s*\(|->selectRaw\s*\(/g;
|
|
16371
|
+
let m;
|
|
16372
|
+
while ((m = sinkRe.exec(code)) !== null) {
|
|
16373
|
+
const ln = lineNo2(code, m.index);
|
|
16374
|
+
const l = ls[ln - 1] ?? "";
|
|
16375
|
+
const scope = ls.slice(Math.max(0, ln - 6), ln + 1).join(`
|
|
16376
|
+
`);
|
|
16377
|
+
if (/->prepare\s*\(/.test(scope))
|
|
16378
|
+
continue;
|
|
16379
|
+
if (SOURCE_TOKENS3.test(scope))
|
|
16380
|
+
continue;
|
|
16381
|
+
const inlineConcat = /\.\s*\$[A-Za-z_]\w*/.test(l);
|
|
16382
|
+
const assignConcat = /(?:^|\n)\s*\$[A-Za-z_]\w*\s*(?:\.=|=)\s*[^;\n]*["'][^"']*["']\s*\.\s*\$[A-Za-z_]\w*/.test(scope);
|
|
16383
|
+
if (!inlineConcat && !assignConcat)
|
|
16384
|
+
continue;
|
|
16385
|
+
out.push({
|
|
16386
|
+
file,
|
|
16387
|
+
line: ln,
|
|
16388
|
+
type: "sql_injection",
|
|
16389
|
+
category: "SQL injection (raw string-interpolated query)",
|
|
16390
|
+
severity: "medium",
|
|
16391
|
+
evidence: l.trim(),
|
|
16392
|
+
detail: "A SQL sink receives a query built by string concatenation (not prepared), with no visible source in this file — trace the concatenated variable back to a request source; if user-controlled, this is SQL injection (CWE-89)."
|
|
16393
|
+
});
|
|
16394
|
+
}
|
|
16395
|
+
}
|
|
16396
|
+
function detectHardcodedSecret(code, file, out) {
|
|
16397
|
+
const ls = lines2(code);
|
|
16398
|
+
const clean = stripCommentsOnly(code);
|
|
16399
|
+
const flaggedLines = new Set;
|
|
16400
|
+
SECRET_FIELD_RE.lastIndex = 0;
|
|
16401
|
+
let m;
|
|
16402
|
+
while ((m = SECRET_FIELD_RE.exec(clean)) !== null) {
|
|
16403
|
+
const field = m[1];
|
|
16404
|
+
const rest = clean.slice(m.index + m[0].length);
|
|
16405
|
+
const lineEnd = rest.search(/\r?\n/);
|
|
16406
|
+
const valueRaw = (lineEnd === -1 ? rest : rest.slice(0, lineEnd)).trim();
|
|
16407
|
+
let value = "";
|
|
16408
|
+
const qm = valueRaw.match(/^["']([^"']{4,})["']/);
|
|
16409
|
+
if (qm)
|
|
16410
|
+
value = qm[1];
|
|
16411
|
+
else
|
|
16412
|
+
value = (valueRaw.match(/^([A-Za-z0-9._/+=@-]{8,})/) ?? [])[1] ?? "";
|
|
16413
|
+
if (!value)
|
|
16414
|
+
continue;
|
|
16415
|
+
if (SECRET_PLACEHOLDER_RE.test(value))
|
|
16416
|
+
continue;
|
|
16417
|
+
if (/\$[A-Za-z_]|[{}\[\]]|process\.env|getenv|os\.environ|config\s*\(|env\s*\(/.test(valueRaw))
|
|
16418
|
+
continue;
|
|
16419
|
+
const ln = lineNo2(code, m.index);
|
|
16420
|
+
flaggedLines.add(ln);
|
|
16421
|
+
out.push({
|
|
16422
|
+
file,
|
|
16423
|
+
line: ln,
|
|
16424
|
+
type: "hardcoded_secret",
|
|
16425
|
+
category: "Hardcoded secret (credential in source)",
|
|
16426
|
+
severity: "high",
|
|
16427
|
+
evidence: (ls[ln - 1] ?? "").trim(),
|
|
16428
|
+
detail: `Secret field '${field}' is assigned a hardcoded value in source — the credential is exposed to anyone with code access (CWE-798 Hard-coded Credentials). Rotate it and load from the environment instead.`
|
|
16429
|
+
});
|
|
16430
|
+
}
|
|
16431
|
+
for (const fmt of SECRET_FORMATS) {
|
|
16432
|
+
fmt.re.lastIndex = 0;
|
|
16433
|
+
let fm;
|
|
16434
|
+
while ((fm = fmt.re.exec(clean)) !== null) {
|
|
16435
|
+
const ln = lineNo2(code, fm.index);
|
|
16436
|
+
if (flaggedLines.has(ln))
|
|
16437
|
+
continue;
|
|
16438
|
+
flaggedLines.add(ln);
|
|
16439
|
+
out.push({
|
|
16440
|
+
file,
|
|
16441
|
+
line: ln,
|
|
16442
|
+
type: "hardcoded_secret",
|
|
16443
|
+
category: "Hardcoded secret (known format)",
|
|
16444
|
+
severity: "high",
|
|
16445
|
+
evidence: (ls[ln - 1] ?? "").trim(),
|
|
16446
|
+
detail: `A ${fmt.name} was found hardcoded in source — the credential is exposed to anyone with code access (CWE-798). Rotate it and load from the environment instead.`
|
|
16447
|
+
});
|
|
16448
|
+
}
|
|
16449
|
+
}
|
|
16450
|
+
const isMinified = /\.min\.(js|css)$/i.test(file) || /[\\/](bundle|vendor|dist|build)[\\/]/i.test(file);
|
|
16451
|
+
if (!isMinified) {
|
|
16452
|
+
const entropyRe = /(\b[A-Za-z_]\w*)\s*(?:::|=>|=|:)\s*["']([A-Za-z0-9_-]{32,})["']/g;
|
|
16453
|
+
entropyRe.lastIndex = 0;
|
|
16454
|
+
let em;
|
|
16455
|
+
while ((em = entropyRe.exec(clean)) !== null) {
|
|
16456
|
+
const varName = em[1];
|
|
16457
|
+
const value = em[2];
|
|
16458
|
+
const ln = lineNo2(code, em.index);
|
|
16459
|
+
if (flaggedLines.has(ln))
|
|
16460
|
+
continue;
|
|
16461
|
+
if (ENTROPY_EXCLUDE_VAR.test(varName))
|
|
16462
|
+
continue;
|
|
16463
|
+
if (!/[A-Z]/.test(value) || !/[a-z]/.test(value) || !/[0-9]/.test(value))
|
|
16464
|
+
continue;
|
|
16465
|
+
if (shannonEntropy(value) < 4.5)
|
|
16466
|
+
continue;
|
|
16467
|
+
flaggedLines.add(ln);
|
|
16468
|
+
out.push({
|
|
16469
|
+
file,
|
|
16470
|
+
line: ln,
|
|
16471
|
+
type: "hardcoded_secret",
|
|
16472
|
+
category: "Suspected secret (high-entropy token)",
|
|
16473
|
+
severity: "medium",
|
|
16474
|
+
evidence: (ls[ln - 1] ?? "").trim(),
|
|
16475
|
+
detail: `'${varName}' is assigned a high-entropy token (Shannon ${shannonEntropy(value).toFixed(2)} bits/char) that looks like a generated credential — verify it is not a secret key/token (CWE-798).`
|
|
16476
|
+
});
|
|
16477
|
+
}
|
|
16478
|
+
}
|
|
16479
|
+
}
|
|
16480
|
+
function detectLlmInjection(code, file, out) {
|
|
16481
|
+
const ls = lines2(code);
|
|
16482
|
+
const clean = stripCommentsOnly(code);
|
|
16483
|
+
const llmCallRe = /\b(?:openai|anthropic|langchain|llama_index)\.[A-Za-z_.]+\s*\(|\b(?:ChatCompletion|completion|messages)\.create\s*\(|\bllm\.invoke\s*\(|\.chat\.completions\.create\s*\(/g;
|
|
16484
|
+
let m;
|
|
16485
|
+
while ((m = llmCallRe.exec(clean)) !== null) {
|
|
16486
|
+
const ln = lineNo2(code, m.index);
|
|
16487
|
+
const scope = ls.slice(Math.max(0, ln - 6), ln + 3).join(`
|
|
16488
|
+
`);
|
|
16489
|
+
if (!SOURCE_TOKENS3.test(scope))
|
|
16490
|
+
continue;
|
|
16491
|
+
out.push({
|
|
16492
|
+
file,
|
|
16493
|
+
line: ln,
|
|
16494
|
+
type: "llm_injection",
|
|
16495
|
+
category: "LLM prompt injection surface",
|
|
16496
|
+
severity: "medium",
|
|
16497
|
+
evidence: (ls[ln - 1] ?? "").trim(),
|
|
16498
|
+
detail: "Request input reaches an LLM call (chat/completion/invoke) with no visible prompt-injection boundary — untrusted text may override instructions or exfiltrate context (OWASP LLM01 Prompt Injection). Verify the prompt is sandboxed and output is not used for privileged actions."
|
|
16499
|
+
});
|
|
16500
|
+
}
|
|
16501
|
+
}
|
|
16502
|
+
function detectGraphqlExposure(code, file, out) {
|
|
16503
|
+
const ls = lines2(code);
|
|
16504
|
+
const clean = stripCommentsOnly(code);
|
|
16505
|
+
const re = /\b(introspection|playground|graphiql)\s*[:=]\s*(true|1)\b/gi;
|
|
16506
|
+
let m;
|
|
16507
|
+
while ((m = re.exec(clean)) !== null) {
|
|
16508
|
+
const ln = lineNo2(code, m.index);
|
|
16509
|
+
out.push({
|
|
16510
|
+
file,
|
|
16511
|
+
line: ln,
|
|
16512
|
+
type: "graphql_exposure",
|
|
16513
|
+
category: "GraphQL introspection/playground exposed",
|
|
16514
|
+
severity: "medium",
|
|
16515
|
+
evidence: (ls[ln - 1] ?? "").trim(),
|
|
16516
|
+
detail: `GraphQL ${m[1].toLowerCase()} is enabled (${m[2]}) in a configuration that appears production-reachable — introspection exposes the full schema/fields, and a playground enables interactive querying (CWE-200 Information Exposure). Disable in production.`
|
|
16517
|
+
});
|
|
16518
|
+
}
|
|
16519
|
+
}
|
|
16520
|
+
function detectOauthMisconfig(code, file, out) {
|
|
16521
|
+
const ls = lines2(code);
|
|
16522
|
+
const clean = stripCommentsOnly(code);
|
|
16523
|
+
const re = /\b(redirect_uri|redirectUrl|return_url|returnUrl|callback_url)\s*[:=]\s*/gi;
|
|
16524
|
+
let m;
|
|
16525
|
+
while ((m = re.exec(clean)) !== null) {
|
|
16526
|
+
const ln = lineNo2(code, m.index);
|
|
16527
|
+
const rest = clean.slice(m.index + m[0].length);
|
|
16528
|
+
const lineEnd = rest.search(/\r?\n/);
|
|
16529
|
+
const line = lineEnd === -1 ? rest : rest.slice(0, lineEnd);
|
|
16530
|
+
if (!/(request|params|query|\$_(?:GET|POST|REQUEST)|req\.)/i.test(line))
|
|
16531
|
+
continue;
|
|
16532
|
+
const scope = ls.slice(Math.max(0, ln - 6), ln + 3).join(`
|
|
16533
|
+
`);
|
|
16534
|
+
if (/(allowlist|whitelist|allowed_|in_array|startsWith|===\s*['"])/.test(scope))
|
|
16535
|
+
continue;
|
|
16536
|
+
out.push({
|
|
16537
|
+
file,
|
|
16538
|
+
line: ln,
|
|
16539
|
+
type: "oauth_misconfig",
|
|
16540
|
+
category: "OAuth redirect_uri misconfiguration",
|
|
16541
|
+
severity: "high",
|
|
16542
|
+
evidence: (ls[ln - 1] ?? "").trim(),
|
|
16543
|
+
detail: "An OAuth redirect_uri/callback is fed by request input with no allowlist validation — an attacker can redirect the authorization code/token to an attacker-controlled endpoint (CWE-601 Open Redirect → token theft). Validate the redirect target against a static allowlist."
|
|
16544
|
+
});
|
|
16545
|
+
}
|
|
16546
|
+
}
|
|
16547
|
+
function detectGrpcReflection(code, file, out) {
|
|
16548
|
+
const ls = lines2(code);
|
|
16549
|
+
const clean = stripCommentsOnly(code);
|
|
16550
|
+
const re = /\b(enable_server_reflection|RegisterReflectionService|reflection\.enable|grpc_reflection|reflection\.Register)\b/gi;
|
|
16551
|
+
let m;
|
|
16552
|
+
while ((m = re.exec(clean)) !== null) {
|
|
16553
|
+
const ln = lineNo2(code, m.index);
|
|
16554
|
+
out.push({
|
|
16555
|
+
file,
|
|
16556
|
+
line: ln,
|
|
16557
|
+
type: "grpc_reflection",
|
|
16558
|
+
category: "gRPC reflection exposed",
|
|
16559
|
+
severity: "low",
|
|
16560
|
+
evidence: (ls[ln - 1] ?? "").trim(),
|
|
16561
|
+
detail: "gRPC server reflection is enabled — it enumerates every service/method to any caller, expanding the attack surface and leaking the API contract (CWE-200 Information Exposure). Disable reflection in production."
|
|
16562
|
+
});
|
|
16563
|
+
}
|
|
16564
|
+
}
|
|
16565
|
+
var SOURCE_TOKENS3, MAGIC_METHODS, SECRET_FIELD_RE, SECRET_PLACEHOLDER_RE, ENTROPY_EXCLUDE_VAR;
|
|
16566
|
+
var init_complex_bugs = __esm(() => {
|
|
16567
|
+
init_security_state();
|
|
16568
|
+
init_framework_security();
|
|
16569
|
+
init_secrets();
|
|
16570
|
+
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)/;
|
|
16571
|
+
MAGIC_METHODS = /\b(__destruct|__wakeup|__toString|__invoke|__call|__get|__set|__isset|__unset|__sleep)\s*\(/;
|
|
16572
|
+
SECRET_FIELD_RE = new RegExp(`\\b(${SECRET_FIELDS})\\s*(?:::|=>|=|:)\\s*`, "gi");
|
|
16573
|
+
SECRET_PLACEHOLDER_RE = /^(null|true|false|undefined|your_|xxx+|changeme|example|placeholder|test|secret|password|token|key|api|smtp|localhost|root|admin|pass|change|me|none|na|foo|bar|dummy)$/i;
|
|
16574
|
+
ENTROPY_EXCLUDE_VAR = /\b(data|payload|content|body|base64|encoded|encode|image|binary|blob|hash|digest|checksum|nonce|uuid|etag|signature|sig|cipher|iv|ciphertext|plaintext|thumb|thumbnail)\b/i;
|
|
16575
|
+
});
|
|
16576
|
+
|
|
16577
|
+
// src/verify-harness.ts
|
|
16578
|
+
function runVerificationHarness() {
|
|
16579
|
+
const cases = CASES.map((c) => {
|
|
16580
|
+
const pos = detectComplexBugs(c.positive, "positive.php").filter((f) => f.type === c.detector);
|
|
16581
|
+
const neg = detectComplexBugs(c.negative, "negative.php").filter((f) => f.type === c.detector);
|
|
16582
|
+
const positive_fired = pos.length > 0;
|
|
16583
|
+
const negative_fired = neg.length > 0;
|
|
16584
|
+
const tp = positive_fired;
|
|
16585
|
+
const tn = !negative_fired;
|
|
16586
|
+
const status = tp && tn ? "pass" : !tp ? "fn" : "fp";
|
|
16587
|
+
return { id: c.id, detector: c.detector, positive_fired, negative_fired, tp, tn, status };
|
|
16588
|
+
});
|
|
16589
|
+
const passed = cases.filter((c) => c.status === "pass").length;
|
|
16590
|
+
return {
|
|
16591
|
+
total: cases.length,
|
|
16592
|
+
passed,
|
|
16593
|
+
failed: cases.length - passed,
|
|
16594
|
+
tp_count: cases.filter((c) => c.tp).length,
|
|
16595
|
+
fp_count: cases.filter((c) => c.status === "fp").length,
|
|
16596
|
+
fn_count: cases.filter((c) => c.status === "fn").length,
|
|
16597
|
+
cases
|
|
16598
|
+
};
|
|
16599
|
+
}
|
|
16600
|
+
function verificationReportText() {
|
|
16601
|
+
const r = runVerificationHarness();
|
|
16602
|
+
const lines = [];
|
|
16603
|
+
lines.push(`Blitz Strike verification harness`);
|
|
16604
|
+
lines.push(` ${r.passed}/${r.total} detectors pass (TP + TN) · TP ${r.tp_count} · FP ${r.fp_count} · FN ${r.fn_count}`);
|
|
16605
|
+
lines.push(``);
|
|
16606
|
+
for (const c of r.cases) {
|
|
16607
|
+
const mark = c.status === "pass" ? "OK " : c.status === "fn" ? "FN " : "FP ";
|
|
16608
|
+
lines.push(` [${mark}] ${c.id.padEnd(20)} pos=${c.positive_fired ? "hit" : "miss"} neg=${c.negative_fired ? "HIT" : "clean"}`);
|
|
16609
|
+
}
|
|
16610
|
+
if (r.failed > 0) {
|
|
16611
|
+
lines.push(``);
|
|
16612
|
+
lines.push(` ${r.failed} detector(s) failed — see the cases above (FN = missed a positive, FP = fired on clean code).`);
|
|
16613
|
+
}
|
|
16614
|
+
return lines.join(`
|
|
16615
|
+
`);
|
|
16616
|
+
}
|
|
16617
|
+
var PHP = (body) => `<?php
|
|
16618
|
+
${body}
|
|
16619
|
+
`, CASES;
|
|
16620
|
+
var init_verify_harness = __esm(() => {
|
|
16621
|
+
init_complex_bugs();
|
|
16622
|
+
CASES = [
|
|
16623
|
+
{
|
|
16624
|
+
id: "deserialization",
|
|
16625
|
+
detector: "deserialization",
|
|
16626
|
+
positive: PHP(`$o = unserialize($_GET['data']);
|
|
16627
|
+
class A { function __destruct() { system($this->c); } }`),
|
|
16628
|
+
negative: PHP(`$o = json_decode($_GET['data'], true);`)
|
|
16629
|
+
},
|
|
16630
|
+
{
|
|
16631
|
+
id: "type-juggling",
|
|
16632
|
+
detector: "type_juggling",
|
|
16633
|
+
positive: PHP(`$hash = "0e1234567890";
|
|
16634
|
+
if ($_GET['pwd'] == $hash) { login(); }`),
|
|
16635
|
+
negative: PHP(`if (hash_equals($_GET['pwd'], $hash)) { login(); }`)
|
|
16636
|
+
},
|
|
16637
|
+
{
|
|
16638
|
+
id: "mass-assignment",
|
|
16639
|
+
detector: "mass_assignment",
|
|
16640
|
+
positive: PHP(`extract($_POST);`),
|
|
16641
|
+
negative: PHP(`extract($data, EXTR_SKIP);`)
|
|
16642
|
+
},
|
|
16643
|
+
{
|
|
16644
|
+
id: "prototype-pollution",
|
|
16645
|
+
detector: "prototype_pollution",
|
|
16646
|
+
positive: PHP(`const merged = deepMerge({}, JSON.parse(req.body));`),
|
|
16647
|
+
negative: PHP(`const merged = Object.assign({}, { a: 1 });`)
|
|
16648
|
+
},
|
|
16649
|
+
{
|
|
16650
|
+
id: "crlf-injection",
|
|
16651
|
+
detector: "crlf_injection",
|
|
16652
|
+
positive: PHP(`header("Location: " . $_GET['url']);`),
|
|
16653
|
+
negative: PHP(`header("Location: /fixed");`)
|
|
16654
|
+
},
|
|
16655
|
+
{
|
|
16656
|
+
id: "path-traversal",
|
|
16657
|
+
detector: "path_confusion",
|
|
16658
|
+
positive: PHP(`include($_GET['page'] . ".php");`),
|
|
16659
|
+
negative: PHP(`include('views/' . basename($_GET['page']) . '.php');`)
|
|
16660
|
+
},
|
|
16661
|
+
{
|
|
16662
|
+
id: "ssrf",
|
|
16663
|
+
detector: "ssrf",
|
|
16664
|
+
positive: PHP(`$r = file_get_contents($_GET['url']);`),
|
|
16665
|
+
negative: PHP(`$r = file_get_contents('https://api.fixed.example/x');`)
|
|
16666
|
+
},
|
|
16667
|
+
{
|
|
16668
|
+
id: "xxe",
|
|
16669
|
+
detector: "xxe",
|
|
16670
|
+
positive: PHP(`$x = simplexml_load_string($_POST['xml']);`),
|
|
16671
|
+
negative: PHP(`$x = json_decode($_POST['data']);`)
|
|
16672
|
+
},
|
|
16673
|
+
{
|
|
16674
|
+
id: "ssti",
|
|
16675
|
+
detector: "ssti",
|
|
16676
|
+
positive: PHP(`return render_template_string($_GET['tpl']);`),
|
|
16677
|
+
negative: PHP(`return render_template('fixed.html');`)
|
|
16678
|
+
},
|
|
16679
|
+
{
|
|
16680
|
+
id: "wrong-sanitizer",
|
|
16681
|
+
detector: "wrong_sanitizer",
|
|
16682
|
+
positive: PHP(`$x = htmlspecialchars($_GET['q']);
|
|
16683
|
+
$wpdb->query("SELECT * FROM t WHERE c='" . $x . "'");`),
|
|
16684
|
+
negative: PHP(`$x = $wpdb->prepare("SELECT * FROM t WHERE c=%s", $_GET['q']);`)
|
|
16685
|
+
},
|
|
16686
|
+
{
|
|
16687
|
+
id: "missing-authz",
|
|
16688
|
+
detector: "missing_authz",
|
|
16689
|
+
positive: PHP(`add_action('wp_ajax_nopriv_set_opt', 'h');
|
|
16690
|
+
function h() { update_option('x', $_POST['x']); }`),
|
|
16691
|
+
negative: PHP(`add_action('wp_ajax_set_opt', 'h');
|
|
16692
|
+
function h() { if (!current_user_can('manage_options')) wp_die(); update_option('x', $_POST['x']); }`)
|
|
16693
|
+
},
|
|
16694
|
+
{
|
|
16695
|
+
id: "missing-nonce",
|
|
16696
|
+
detector: "missing_nonce",
|
|
16697
|
+
positive: PHP(`add_action('wp_ajax_del', 'h');
|
|
16698
|
+
function h() { wp_delete_post((int)$_POST['id']); }`),
|
|
16699
|
+
negative: PHP(`add_action('wp_ajax_del', 'h');
|
|
16700
|
+
function h() { check_ajax_referer('n'); wp_delete_post((int)$_POST['id']); }`)
|
|
16701
|
+
},
|
|
16702
|
+
{
|
|
16703
|
+
id: "file-upload",
|
|
16704
|
+
detector: "file_upload",
|
|
16705
|
+
positive: PHP(`$f = $_FILES['up'];
|
|
16706
|
+
move_uploaded_file($f['tmp_name'], '/uploads/' . $f['name']);`),
|
|
16707
|
+
negative: PHP(`$f = $_FILES['up'];
|
|
16708
|
+
if (in_array(pathinfo($f['name'], PATHINFO_EXTENSION), ['jpg','png'])) move_uploaded_file($f['tmp_name'], '/uploads/' . $f['name']);`)
|
|
16709
|
+
},
|
|
16710
|
+
{
|
|
16711
|
+
id: "sql-injection",
|
|
16712
|
+
detector: "sql_injection",
|
|
16713
|
+
positive: PHP(`$wpdb->get_results("SELECT * FROM t WHERE id=" . $id);`),
|
|
16714
|
+
negative: PHP(`$wpdb->get_results($wpdb->prepare("SELECT * FROM t WHERE id=%d", $id));`)
|
|
16715
|
+
},
|
|
16716
|
+
{
|
|
16717
|
+
id: "hardcoded-secret",
|
|
16718
|
+
detector: "hardcoded_secret",
|
|
16719
|
+
positive: PHP(`$api_key = "EXAMPLE_API_KEY_123456789";`),
|
|
16720
|
+
negative: PHP(`$api_key = getenv("API_KEY");`)
|
|
16721
|
+
},
|
|
16722
|
+
{
|
|
16723
|
+
id: "llm-injection",
|
|
16724
|
+
detector: "llm_injection",
|
|
16725
|
+
positive: PHP(`$u = $_POST['msg'];
|
|
16726
|
+
$r = openai.chat.completions.create(['messages' => [['role' => 'user', 'content' => $u]]]);`),
|
|
16727
|
+
negative: PHP(`$r = openai.chat.completions.create(['messages' => [['role' => 'user', 'content' => 'hello']]]);`)
|
|
16728
|
+
},
|
|
16729
|
+
{
|
|
16730
|
+
id: "graphql-exposure",
|
|
16731
|
+
detector: "graphql_exposure",
|
|
16732
|
+
positive: PHP(`$srv = new ApolloServer({ introspection: true, playground: true });`),
|
|
16733
|
+
negative: PHP(`$srv = new ApolloServer({ introspection: false });`)
|
|
16734
|
+
},
|
|
16735
|
+
{
|
|
16736
|
+
id: "oauth-misconfig",
|
|
16737
|
+
detector: "oauth_misconfig",
|
|
16738
|
+
positive: PHP(`$redirect_uri = $_GET['redirect_uri'];`),
|
|
16739
|
+
negative: PHP(`$redirect_uri = 'https://app.example.com/callback';`)
|
|
16740
|
+
},
|
|
16741
|
+
{
|
|
16742
|
+
id: "grpc-reflection",
|
|
16743
|
+
detector: "grpc_reflection",
|
|
16744
|
+
positive: PHP(`$server->addService(enable_server_reflection());`),
|
|
16745
|
+
negative: PHP(`$server->addService($my_service);`)
|
|
16746
|
+
}
|
|
16747
|
+
];
|
|
16748
|
+
});
|
|
16749
|
+
|
|
16750
|
+
// src/sync.ts
|
|
16751
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, readFileSync as readFileSync24, readdirSync as readdirSync7, statSync as statSync4, rmSync as rmSync4 } from "node:fs";
|
|
16752
|
+
import { join as join21 } from "node:path";
|
|
16753
|
+
import { homedir as homedir10 } from "node:os";
|
|
16754
|
+
import { fileURLToPath as fileURLToPath11 } from "node:url";
|
|
16755
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
16756
|
+
function dirExists(p) {
|
|
16757
|
+
try {
|
|
16758
|
+
return statSync4(p).isDirectory();
|
|
16759
|
+
} catch {
|
|
16760
|
+
return false;
|
|
16761
|
+
}
|
|
16762
|
+
}
|
|
16763
|
+
function countFiles(dir) {
|
|
16764
|
+
try {
|
|
16765
|
+
let n = 0;
|
|
16766
|
+
const walk = (d) => {
|
|
16767
|
+
for (const e of readdirSync7(d, { withFileTypes: true })) {
|
|
16768
|
+
if (e.isDirectory())
|
|
16769
|
+
walk(join21(d, e.name));
|
|
16770
|
+
else
|
|
16771
|
+
n++;
|
|
16772
|
+
}
|
|
16773
|
+
};
|
|
16774
|
+
walk(dir);
|
|
16775
|
+
return n;
|
|
16776
|
+
} catch {
|
|
16777
|
+
return 0;
|
|
16778
|
+
}
|
|
16779
|
+
}
|
|
16780
|
+
function syncData() {
|
|
16781
|
+
if (!dirExists(DATA_ROOT2))
|
|
16782
|
+
mkdirSync9(DATA_ROOT2, { recursive: true });
|
|
16783
|
+
const payloadsPath = join21(DATA_ROOT2, "payloads");
|
|
16784
|
+
const templatesPath = join21(DATA_ROOT2, "templates");
|
|
16785
|
+
const hasPayloads = dirExists(payloadsPath) && countFiles(payloadsPath) > 0;
|
|
16786
|
+
const hasTemplates = dirExists(templatesPath) && countFiles(templatesPath) > 0;
|
|
16787
|
+
if (hasPayloads && hasTemplates) {
|
|
16788
|
+
return {
|
|
16789
|
+
payloads: { path: payloadsPath, files: countFiles(payloadsPath), synced: false },
|
|
16790
|
+
templates: { path: templatesPath, files: countFiles(templatesPath), synced: false },
|
|
16791
|
+
data_root: DATA_ROOT2
|
|
16792
|
+
};
|
|
16793
|
+
}
|
|
16794
|
+
const localPayloads = join21(ROOT7, "payloads");
|
|
16795
|
+
const localTemplates = join21(ROOT7, "templates");
|
|
16796
|
+
let payloadsSynced = false;
|
|
16797
|
+
let templatesSynced = false;
|
|
16798
|
+
if (!hasPayloads && dirExists(localPayloads)) {
|
|
16799
|
+
copyDir(localPayloads, payloadsPath);
|
|
16800
|
+
payloadsSynced = true;
|
|
16801
|
+
}
|
|
16802
|
+
if (!hasTemplates && dirExists(localTemplates)) {
|
|
16803
|
+
copyDir(localTemplates, templatesPath);
|
|
16804
|
+
templatesSynced = true;
|
|
16805
|
+
}
|
|
16806
|
+
const gitOk = spawnSync4("git", ["--version"]).status === 0;
|
|
16807
|
+
if (gitOk && (!dirExists(payloadsPath) || countFiles(payloadsPath) === 0 || !dirExists(templatesPath) || countFiles(templatesPath) === 0)) {
|
|
16808
|
+
const tmp = join21(DATA_ROOT2, ".tmp-clone");
|
|
16809
|
+
if (dirExists(tmp))
|
|
16810
|
+
rmSync4(tmp, { recursive: true, force: true });
|
|
16811
|
+
const r = spawnSync4("git", ["clone", "--depth", "1", "--filter=blob:none", "--sparse", GITHUB_REPO, tmp], { timeout: 120000 });
|
|
16812
|
+
if (r.status === 0) {
|
|
16813
|
+
const sparse = spawnSync4("git", ["-C", tmp, "sparse-checkout", "set", "payloads", "templates"], { timeout: 120000 });
|
|
16814
|
+
if (sparse.status === 0) {
|
|
16815
|
+
if (!dirExists(payloadsPath) || countFiles(payloadsPath) === 0) {
|
|
16816
|
+
copyDir(join21(tmp, "payloads"), payloadsPath);
|
|
16817
|
+
payloadsSynced = true;
|
|
16818
|
+
}
|
|
16819
|
+
if (!dirExists(templatesPath) || countFiles(templatesPath) === 0) {
|
|
16820
|
+
copyDir(join21(tmp, "templates"), templatesPath);
|
|
16821
|
+
templatesSynced = true;
|
|
16822
|
+
}
|
|
16823
|
+
}
|
|
16824
|
+
rmSync4(tmp, { recursive: true, force: true });
|
|
16825
|
+
}
|
|
16826
|
+
}
|
|
16827
|
+
return {
|
|
16828
|
+
payloads: { path: payloadsPath, files: countFiles(payloadsPath), synced: payloadsSynced },
|
|
16829
|
+
templates: { path: templatesPath, files: countFiles(templatesPath), synced: templatesSynced },
|
|
16830
|
+
data_root: DATA_ROOT2
|
|
16831
|
+
};
|
|
16832
|
+
}
|
|
16833
|
+
function copyDir(src, dst) {
|
|
16834
|
+
const r = spawnSync4("cp", ["-r", src, dst], { timeout: 120000 });
|
|
16835
|
+
if (r.status !== 0) {
|
|
16836
|
+
manualCopy(src, dst);
|
|
16837
|
+
}
|
|
16838
|
+
}
|
|
16839
|
+
function manualCopy(src, dst) {
|
|
16840
|
+
mkdirSync9(dst, { recursive: true });
|
|
16841
|
+
for (const e of readdirSync7(src, { withFileTypes: true })) {
|
|
16842
|
+
const s = join21(src, e.name);
|
|
16843
|
+
const d = join21(dst, e.name);
|
|
16844
|
+
if (e.isDirectory())
|
|
16845
|
+
manualCopy(s, d);
|
|
16846
|
+
else
|
|
16847
|
+
writeFileSync8(d, readFileSync24(s));
|
|
16848
|
+
}
|
|
16849
|
+
}
|
|
16850
|
+
var ROOT7, DATA_ROOT2, GITHUB_REPO = "https://github.com/shinthink/blitzstrike.git";
|
|
16851
|
+
var init_sync = __esm(() => {
|
|
16852
|
+
ROOT7 = join21(fileURLToPath11(new URL(".", import.meta.url)), "..");
|
|
16853
|
+
DATA_ROOT2 = process.env.BLITZSTRIKE_DATA ?? join21(homedir10(), ".blitzstrike", "data");
|
|
16854
|
+
});
|
|
16855
|
+
|
|
16856
|
+
// src/update.ts
|
|
16857
|
+
async function checkLatestVersion(timeoutMs = 1e4) {
|
|
16858
|
+
try {
|
|
16859
|
+
const controller = new AbortController;
|
|
16860
|
+
const t = setTimeout(() => controller.abort(), timeoutMs);
|
|
16861
|
+
const res = await fetch("https://registry.npmjs.org/blitzstrike/latest", { signal: controller.signal });
|
|
16862
|
+
clearTimeout(t);
|
|
16863
|
+
if (!res.ok)
|
|
16864
|
+
return null;
|
|
16865
|
+
const data = await res.json();
|
|
16866
|
+
return data.version ?? null;
|
|
16867
|
+
} catch {
|
|
16868
|
+
return null;
|
|
16869
|
+
}
|
|
16870
|
+
}
|
|
16871
|
+
function semverCompare(a, b) {
|
|
16872
|
+
const pa = a.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
|
|
16873
|
+
const pb = b.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
|
|
16874
|
+
for (let i = 0;i < 3; i++) {
|
|
16875
|
+
const x = pa[i] ?? 0;
|
|
16876
|
+
const y = pb[i] ?? 0;
|
|
16877
|
+
if (x !== y)
|
|
16878
|
+
return x < y ? -1 : 1;
|
|
16879
|
+
}
|
|
16880
|
+
return 0;
|
|
16881
|
+
}
|
|
16882
|
+
async function runUpdate(currentVersion) {
|
|
16883
|
+
const latest = await checkLatestVersion();
|
|
16884
|
+
const outdated = latest !== null && semverCompare(currentVersion, latest) < 0;
|
|
16885
|
+
return {
|
|
16886
|
+
current: currentVersion,
|
|
16887
|
+
latest,
|
|
16888
|
+
outdated,
|
|
16889
|
+
update_command: "npm install -g blitzstrike@latest # or: bun add -g blitzstrike@latest",
|
|
16890
|
+
data_sync: syncData()
|
|
16891
|
+
};
|
|
16892
|
+
}
|
|
16893
|
+
var init_update = __esm(() => {
|
|
16894
|
+
init_sync();
|
|
16895
|
+
});
|
|
16896
|
+
|
|
16897
|
+
// src/release.ts
|
|
16898
|
+
import { readFileSync as readFileSync26, existsSync as existsSync12 } from "node:fs";
|
|
16899
|
+
import { join as join22 } from "node:path";
|
|
16900
|
+
import { fileURLToPath as fileURLToPath12 } from "node:url";
|
|
16901
|
+
import { execSync as execSync2 } from "node:child_process";
|
|
16902
|
+
function validateRelease() {
|
|
16903
|
+
const checks = [];
|
|
16904
|
+
checks.push({ name: "package.json version", ok: true, detail: VERSION });
|
|
16905
|
+
try {
|
|
16906
|
+
const changelog = readFileSync26(join22(ROOT8, "CHANGELOG.md"), "utf8");
|
|
16907
|
+
const m = changelog.match(/^##\s+\[([0-9]+\.[0-9]+\.[0-9]+)\]/m);
|
|
16908
|
+
const changelogVersion = m?.[1] ?? null;
|
|
16909
|
+
checks.push({
|
|
16910
|
+
name: "CHANGELOG version",
|
|
16911
|
+
ok: changelogVersion === VERSION,
|
|
16912
|
+
detail: changelogVersion ?? "no version header found"
|
|
16913
|
+
});
|
|
16914
|
+
} catch {
|
|
16915
|
+
checks.push({ name: "CHANGELOG version", ok: false, detail: "CHANGELOG.md not found" });
|
|
16916
|
+
}
|
|
16917
|
+
try {
|
|
16918
|
+
const tag = execSync2("git describe --tags --abbrev=0", { cwd: ROOT8, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
16919
|
+
checks.push({ name: "Git tag", ok: tag === `v${VERSION}`, detail: tag, soft: true });
|
|
16920
|
+
} catch {
|
|
16921
|
+
checks.push({ name: "Git tag", ok: true, detail: "unavailable (no git/tags)", soft: true });
|
|
16922
|
+
}
|
|
16923
|
+
try {
|
|
16924
|
+
const published = execSync2("npm view blitzstrike version", { encoding: "utf8", timeout: 8000, stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
16925
|
+
checks.push({ name: "npm registry version", ok: published === VERSION, detail: published, soft: true });
|
|
16926
|
+
} catch {
|
|
16927
|
+
checks.push({ name: "npm registry version", ok: true, detail: "unavailable (offline)", soft: true });
|
|
16928
|
+
}
|
|
16929
|
+
checks.push({
|
|
16930
|
+
name: "Lockfile",
|
|
16931
|
+
ok: lockfilePresent(),
|
|
16932
|
+
detail: lockfilePresent() ? "present (bun.lock / package-lock.json)" : "missing",
|
|
16933
|
+
soft: true
|
|
16934
|
+
});
|
|
16935
|
+
return { version: VERSION, checks, consistent: checks.filter((c) => !c.soft).every((c) => c.ok) };
|
|
16936
|
+
}
|
|
16937
|
+
function lockfilePresent() {
|
|
16938
|
+
return existsSync12(join22(ROOT8, "package-lock.json")) || existsSync12(join22(ROOT8, "bun.lock")) || existsSync12(join22(ROOT8, "bun.lockb"));
|
|
16939
|
+
}
|
|
16940
|
+
var ROOT8;
|
|
16941
|
+
var init_release = __esm(() => {
|
|
16942
|
+
init_version();
|
|
16943
|
+
ROOT8 = join22(fileURLToPath12(new URL(".", import.meta.url)), "..");
|
|
16944
|
+
});
|
|
16945
|
+
|
|
16946
|
+
// src/dependency.ts
|
|
16947
|
+
import { readFileSync as readFileSync27 } from "node:fs";
|
|
16948
|
+
import { join as join23 } from "node:path";
|
|
16949
|
+
import { fileURLToPath as fileURLToPath13 } from "node:url";
|
|
16950
|
+
function checkDependencies() {
|
|
16951
|
+
let pkg = {};
|
|
16952
|
+
try {
|
|
16953
|
+
pkg = JSON.parse(readFileSync27(join23(ROOT9, "package.json"), "utf8"));
|
|
16954
|
+
} catch {
|
|
16955
|
+
return { install_scripts: [], git_dependencies: [], total_dependencies: 0, safe: false };
|
|
16956
|
+
}
|
|
16957
|
+
const installScriptNames = ["preinstall", "install", "postinstall", "prepare"];
|
|
16958
|
+
const installScripts = installScriptNames.filter((n) => pkg.scripts?.[n]).map((n) => ({ name: n, command: pkg.scripts[n] }));
|
|
16959
|
+
const allDeps = {
|
|
16960
|
+
...pkg.dependencies ?? {},
|
|
16961
|
+
...pkg.devDependencies ?? {},
|
|
16962
|
+
...pkg.peerDependencies ?? {},
|
|
16963
|
+
...pkg.optionalDependencies ?? {}
|
|
16964
|
+
};
|
|
16965
|
+
const gitDependencies = Object.entries(allDeps).filter(([, v]) => /^(git\+|git:|github:|gitlab:|bitbucket:|\w+:\/\/)/.test(v)).map(([name, v]) => `${name}@${v}`);
|
|
16966
|
+
const total = Object.keys(allDeps).length;
|
|
16967
|
+
return {
|
|
16968
|
+
install_scripts: installScripts,
|
|
16969
|
+
git_dependencies: gitDependencies,
|
|
16970
|
+
total_dependencies: total,
|
|
16971
|
+
safe: installScripts.length === 0 && gitDependencies.length === 0
|
|
16972
|
+
};
|
|
16973
|
+
}
|
|
16974
|
+
var ROOT9;
|
|
16975
|
+
var init_dependency = __esm(() => {
|
|
16976
|
+
ROOT9 = join23(fileURLToPath13(new URL(".", import.meta.url)), "..");
|
|
16977
|
+
});
|
|
16978
|
+
|
|
16979
|
+
// src/cli.ts
|
|
16980
|
+
import { readFileSync as readFileSync28, existsSync as existsSync13, writeFileSync as writeFileSync9, mkdirSync as mkdirSync10 } from "node:fs";
|
|
16981
|
+
import { join as join24, dirname as dirname4 } from "node:path";
|
|
16982
|
+
import { homedir as homedir11 } from "node:os";
|
|
16983
|
+
import { fileURLToPath as fileURLToPath14 } from "node:url";
|
|
16984
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
16985
|
+
function which(cmd) {
|
|
16986
|
+
return spawnSync5("sh", ["-c", 'command -v "$1" >/dev/null 2>&1', "sh", cmd]).status === 0;
|
|
16987
|
+
}
|
|
16988
|
+
function installedCount() {
|
|
16989
|
+
const tools = loadTools();
|
|
16990
|
+
const installed = tools.filter((t) => which(t.name)).map((t) => t.name);
|
|
16991
|
+
return { total: tools.length, installed, missing: tools.map((t) => t.name).filter((n) => !installed.includes(n)) };
|
|
16992
|
+
}
|
|
16993
|
+
async function runDoctor() {
|
|
16994
|
+
console.log(`BlitzStrike doctor — environment health check
|
|
16995
|
+
`);
|
|
16996
|
+
const issues = [];
|
|
16997
|
+
const bunOk = which("bun") || which("node");
|
|
16998
|
+
issues.push({
|
|
16999
|
+
name: "Runtime (bun/node)",
|
|
15742
17000
|
status: bunOk ? "ok" : "fail",
|
|
15743
17001
|
detail: bunOk ? `node ${process.version}` : "neither bun nor node found",
|
|
15744
17002
|
fix: bunOk ? undefined : "install bun: curl -fsSL https://bun.sh/install | bash"
|
|
@@ -30999,70 +32257,9 @@ function recordAudit(event, opts = {}) {
|
|
|
30999
32257
|
|
|
31000
32258
|
// src/evidence.ts
|
|
31001
32259
|
init_version();
|
|
32260
|
+
init_secrets();
|
|
32261
|
+
init_secrets();
|
|
31002
32262
|
import { createHash as createHash2 } from "node:crypto";
|
|
31003
|
-
|
|
31004
|
-
// src/secrets.ts
|
|
31005
|
-
var SECRET_FIELDS = "api[_-]?key|apikey|api[_-]?secret|access[_-]?key|access[_-]?token|secret[_-]?key|secret[_-]?access[_-]?key|auth[_-]?token|client[_-]?secret|consumer[_-]?secret|consumer[_-]?key|private[_-]?key|db[_-]?password|database[_-]?password|db[_-]?pass|passwd|password|jwt[_-]?secret|encryption[_-]?key|encryption[_-]?password|github[_-]?token|gitlab[_-]?token|stripe[_-]?key|sendgrid[_-]?api[_-]?key|datadog[_-]?api[_-]?key|cloudflare[_-]?api[_-]?key|slack[_-]?token|aws[_-]?access[_-]?key[_-]?id|aws[_-]?secret[_-]?access[_-]?key|aws[_-]?session[_-]?token|bot[_-]?token";
|
|
31006
|
-
var SECRET_FORMATS = [
|
|
31007
|
-
{ name: "JWT", re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}/g },
|
|
31008
|
-
{ name: "AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
31009
|
-
{ name: "GitHub PAT (classic)", re: /\bghp_[A-Za-z0-9]{20,}\b/g },
|
|
31010
|
-
{ name: "GitHub PAT (fine-grained)", re: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g },
|
|
31011
|
-
{ name: "OpenAI/Stripe key", re: /\bsk-[A-Za-z0-9]{20,}\b/g },
|
|
31012
|
-
{ name: "Stripe live/test key", re: /\b(?:sk|pk|rk)_(?:live|test)_[0-9a-zA-Z]{10,}\b/g },
|
|
31013
|
-
{ name: "Slack token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
|
|
31014
|
-
{ name: "Google API key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
31015
|
-
{ name: "Twilio account SID", re: /\bAC[0-9a-f]{32}\b/g },
|
|
31016
|
-
{ name: "SendGrid API key", re: /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g },
|
|
31017
|
-
{ name: "Mailgun API key", re: /\bkey-[0-9a-zA-Z]{32}\b/g },
|
|
31018
|
-
{ name: "PEM private key", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/g }
|
|
31019
|
-
];
|
|
31020
|
-
function shannonEntropy(s) {
|
|
31021
|
-
if (!s)
|
|
31022
|
-
return 0;
|
|
31023
|
-
const freq = new Map;
|
|
31024
|
-
for (const c of s)
|
|
31025
|
-
freq.set(c, (freq.get(c) ?? 0) + 1);
|
|
31026
|
-
let e = 0;
|
|
31027
|
-
for (const n of freq.values()) {
|
|
31028
|
-
const p = n / s.length;
|
|
31029
|
-
e -= p * Math.log2(p);
|
|
31030
|
-
}
|
|
31031
|
-
return e;
|
|
31032
|
-
}
|
|
31033
|
-
var REDACTION_PATTERNS = [
|
|
31034
|
-
[/(authorization\s*[:=]\s*)(bearer\s+)?[A-Za-z0-9._~+/=-]{8,}/gi, "$1$2[REDACTED]"],
|
|
31035
|
-
[/(api[_-]?key\s*[:=]\s*)["']?[A-Za-z0-9._-]{16,}["']?/gi, "$1[REDACTED]"],
|
|
31036
|
-
[/(access[_-]?token\s*[:=]\s*)["']?[A-Za-z0-9._-]{16,}["']?/gi, "$1[REDACTED]"],
|
|
31037
|
-
[/(secret\s*[:=]\s*)["']?[A-Za-z0-9._/-]{16,}["']?/gi, "$1[REDACTED]"],
|
|
31038
|
-
[/(password\s*[:=]\s*)["']?[^"'\s,}&]{4,}["']?/gi, "$1[REDACTED]"],
|
|
31039
|
-
[/(passwd\s*[:=]\s*)["']?[^"'\s,}&]{4,}["']?/gi, "$1[REDACTED]"],
|
|
31040
|
-
[/(private[_-]?key\s*[:=]\s*)["']?[A-Za-z0-9+/=_-]{16,}["']?/gi, "$1[REDACTED]"],
|
|
31041
|
-
[/(session\s*[:=]\s*)["']?[A-Za-z0-9._-]{12,}["']?/gi, "$1[REDACTED]"],
|
|
31042
|
-
[/(cookie\s*[:=]\s*)["']?[A-Za-z0-9._-]{12,}["']?/gi, "$1[REDACTED]"],
|
|
31043
|
-
[/(set-cookie\s*:\s*)[^\r\n]+/gi, "$1[REDACTED]"],
|
|
31044
|
-
[/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}/g, "[REDACTED]"],
|
|
31045
|
-
[/AKIA[0-9A-Z]{16}/g, "[REDACTED]"],
|
|
31046
|
-
[/ghp_[A-Za-z0-9]{20,}/g, "[REDACTED]"],
|
|
31047
|
-
[/github_pat_[A-Za-z0-9_]{20,}/g, "[REDACTED]"],
|
|
31048
|
-
[/sk-[A-Za-z0-9]{20,}/g, "[REDACTED]"],
|
|
31049
|
-
[/\b(?:sk|pk|rk)_(?:live|test)_[0-9a-zA-Z]{10,}\b/g, "[REDACTED]"],
|
|
31050
|
-
[/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, "[REDACTED]"],
|
|
31051
|
-
[/\bAIza[0-9A-Za-z_-]{35}\b/g, "[REDACTED]"],
|
|
31052
|
-
[/\bAC[0-9a-f]{32}\b/g, "[REDACTED]"],
|
|
31053
|
-
[/\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g, "[REDACTED]"],
|
|
31054
|
-
[/\bkey-[0-9a-zA-Z]{32}\b/g, "[REDACTED]"],
|
|
31055
|
-
[/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[REDACTED]"]
|
|
31056
|
-
];
|
|
31057
|
-
function redactSecrets(text) {
|
|
31058
|
-
let out = text;
|
|
31059
|
-
for (const [re, repl] of REDACTION_PATTERNS) {
|
|
31060
|
-
out = out.replace(re, repl);
|
|
31061
|
-
}
|
|
31062
|
-
return out;
|
|
31063
|
-
}
|
|
31064
|
-
|
|
31065
|
-
// src/evidence.ts
|
|
31066
32263
|
function hasSecrets(text) {
|
|
31067
32264
|
return redactSecrets(text) !== text;
|
|
31068
32265
|
}
|
|
@@ -31327,6 +32524,7 @@ function rejectFinding(finding, status, reason, evidence) {
|
|
|
31327
32524
|
}
|
|
31328
32525
|
|
|
31329
32526
|
// src/sinks.ts
|
|
32527
|
+
init_secrets();
|
|
31330
32528
|
var NEXT = {
|
|
31331
32529
|
rce: "test the sink with a marker payload (eval -> '1+1' -> 2, then sleep vs no-sleep / write a marker file) — this is direct RCE",
|
|
31332
32530
|
ssti: "probe {{7*7}} / {{7*'7'}} for evaluation; if it evaluates, it is SSTI -> RCE",
|
|
@@ -56084,1097 +57282,179 @@ async function browserAgent(op, args = []) {
|
|
|
56084
57282
|
return browserBack();
|
|
56085
57283
|
case "forward":
|
|
56086
57284
|
return browserForward();
|
|
56087
|
-
case "get_cookie":
|
|
56088
|
-
return browserGetCookie(args[0] ?? "");
|
|
56089
|
-
case "set_cookie":
|
|
56090
|
-
return browserSetCookie(args[0] ?? "", args[1] ?? "");
|
|
56091
|
-
}
|
|
56092
|
-
}
|
|
56093
|
-
function browserDiagnostics() {
|
|
56094
|
-
return { console_errors: [...consoleErrors], page_errors: [...pageErrors] };
|
|
56095
|
-
}
|
|
56096
|
-
|
|
56097
|
-
// src/server.ts
|
|
56098
|
-
init_mcp_status();
|
|
56099
|
-
|
|
56100
|
-
// src/route-confusion.ts
|
|
56101
|
-
var SOURCE_TOKENS = /\$_(GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|\$request->|\$req->|request\.(get|input|query|post)|getParameter\(|req\.(query|body|params)/;
|
|
56102
|
-
function lines(code) {
|
|
56103
|
-
return code.split(`
|
|
56104
|
-
`);
|
|
56105
|
-
}
|
|
56106
|
-
function lineNo(code, pos) {
|
|
56107
|
-
let n = 1;
|
|
56108
|
-
for (let i = 0;i < pos; i++)
|
|
56109
|
-
if (code.charCodeAt(i) === 10)
|
|
56110
|
-
n++;
|
|
56111
|
-
return n;
|
|
56112
|
-
}
|
|
56113
|
-
function userControlled(scope) {
|
|
56114
|
-
return SOURCE_TOKENS.test(scope);
|
|
56115
|
-
}
|
|
56116
|
-
var DYNAMIC_DISPATCH_RE = /\b(call_user_func|call_user_func_array|forward|dispatch|resolve_route|invoke)\s*\(\s*(\$|\[)/gi;
|
|
56117
|
-
var DYNAMIC_METHOD_RE = /(->|::)\s*\$[A-Za-z_]/g;
|
|
56118
|
-
var BATCH_LOOP_RE = /\bforeach\s*\([^)]*(requests|batch|sub_requests|operations)\b/gi;
|
|
56119
|
-
var ROUTE_MATCH_RE = /\b(preg_match|preg_match_all|strpos|stripos|str_contains)\s*\(/i;
|
|
56120
|
-
var NORMALIZERS = /\b(strtolower|strtoupper|rtrim|urldecode|rawurldecode|parse_url|normalize|canonical)\b/i;
|
|
56121
|
-
var WHITELIST_LOOKUP = /\$[A-Za-z_]\w*\s*\[\s*\$[A-Za-z_]\w*\s*\]/;
|
|
56122
|
-
var CANONICALIZE = /\b(basename|realpath|normalize|canonical|resolve|toRealPath)\s*\(/i;
|
|
56123
|
-
function detectDynamicDispatch(code, file, out) {
|
|
56124
|
-
const ls = lines(code);
|
|
56125
|
-
DYNAMIC_DISPATCH_RE.lastIndex = 0;
|
|
56126
|
-
let m;
|
|
56127
|
-
while ((m = DYNAMIC_DISPATCH_RE.exec(code)) !== null) {
|
|
56128
|
-
const ln = lineNo(code, m.index);
|
|
56129
|
-
const l = ls[ln - 1] ?? "";
|
|
56130
|
-
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
56131
|
-
`);
|
|
56132
|
-
if (!userControlled(l) && !userControlled(scope))
|
|
56133
|
-
continue;
|
|
56134
|
-
if (WHITELIST_LOOKUP.test(l) || WHITELIST_LOOKUP.test(scope))
|
|
56135
|
-
continue;
|
|
56136
|
-
out.push({
|
|
56137
|
-
file,
|
|
56138
|
-
line: ln,
|
|
56139
|
-
type: "dynamic_dispatch",
|
|
56140
|
-
category: "Route confusion (user-controlled dispatch)",
|
|
56141
|
-
severity: "high",
|
|
56142
|
-
evidence: l.trim(),
|
|
56143
|
-
detail: `\`${m[1]}()\` invoked with an attacker-controlled callback/route — the request selects the handler.`
|
|
56144
|
-
});
|
|
56145
|
-
}
|
|
56146
|
-
}
|
|
56147
|
-
function detectDynamicMethod(code, file, out) {
|
|
56148
|
-
const ls = lines(code);
|
|
56149
|
-
DYNAMIC_METHOD_RE.lastIndex = 0;
|
|
56150
|
-
let m;
|
|
56151
|
-
while ((m = DYNAMIC_METHOD_RE.exec(code)) !== null) {
|
|
56152
|
-
const ln = lineNo(code, m.index);
|
|
56153
|
-
const l = ls[ln - 1] ?? "";
|
|
56154
|
-
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
56155
|
-
`);
|
|
56156
|
-
if (!userControlled(l) && !userControlled(scope))
|
|
56157
|
-
continue;
|
|
56158
|
-
if (WHITELIST_LOOKUP.test(l) || WHITELIST_LOOKUP.test(scope))
|
|
56159
|
-
continue;
|
|
56160
|
-
out.push({
|
|
56161
|
-
file,
|
|
56162
|
-
line: ln,
|
|
56163
|
-
type: "dynamic_method_call",
|
|
56164
|
-
category: "Route confusion (dynamic method dispatch)",
|
|
56165
|
-
severity: "high",
|
|
56166
|
-
evidence: l.trim(),
|
|
56167
|
-
detail: "Dynamic method/static call `" + m[1] + "$var()` — the invoked symbol is attacker-influenced."
|
|
56168
|
-
});
|
|
56169
|
-
}
|
|
56170
|
-
}
|
|
56171
|
-
function detectDynamicInclude(code, file, out) {
|
|
56172
|
-
const ls = lines(code);
|
|
56173
|
-
const re = /\b(include|require)(_once)?\s*\(\s*(\$[A-Za-z_]\w*)/g;
|
|
56174
|
-
let m;
|
|
56175
|
-
while ((m = re.exec(code)) !== null) {
|
|
56176
|
-
const ln = lineNo(code, m.index);
|
|
56177
|
-
const l = ls[ln - 1] ?? "";
|
|
56178
|
-
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
56179
|
-
`);
|
|
56180
|
-
if (!userControlled(l) && !userControlled(scope))
|
|
56181
|
-
continue;
|
|
56182
|
-
if (CANONICALIZE.test(scope + `
|
|
56183
|
-
` + l))
|
|
56184
|
-
continue;
|
|
56185
|
-
if (WHITELIST_LOOKUP.test(l) || WHITELIST_LOOKUP.test(scope))
|
|
56186
|
-
continue;
|
|
56187
|
-
out.push({
|
|
56188
|
-
file,
|
|
56189
|
-
line: ln,
|
|
56190
|
-
type: "dynamic_include",
|
|
56191
|
-
category: "Route confusion (dynamic file include)",
|
|
56192
|
-
severity: "high",
|
|
56193
|
-
evidence: l.trim(),
|
|
56194
|
-
detail: `\`${m[1]}(…)\` of a variable — potential LFI/RCE when the path is attacker-controlled.`
|
|
56195
|
-
});
|
|
56196
|
-
}
|
|
56197
|
-
}
|
|
56198
|
-
function detectBatchForwarding(code, file, out) {
|
|
56199
|
-
BATCH_LOOP_RE.lastIndex = 0;
|
|
56200
|
-
let m;
|
|
56201
|
-
while ((m = BATCH_LOOP_RE.exec(code)) !== null) {
|
|
56202
|
-
const ln = lineNo(code, m.index);
|
|
56203
|
-
const body = lines(code).slice(ln - 1, ln + 8).join(`
|
|
56204
|
-
`);
|
|
56205
|
-
const forwards = /\b(call_user_func|forward|dispatch|resolve_route|invoke|handle|route)\s*\(/i.test(body);
|
|
56206
|
-
if (!forwards)
|
|
56207
|
-
continue;
|
|
56208
|
-
const rechecks = /\b(current_user_can|permission_callback|check_ajax_referer|authorize|authorise|is_user_logged_in)\b/i.test(body);
|
|
56209
|
-
if (rechecks)
|
|
56210
|
-
continue;
|
|
56211
|
-
out.push({
|
|
56212
|
-
file,
|
|
56213
|
-
line: ln,
|
|
56214
|
-
type: "batch_forwarding",
|
|
56215
|
-
category: "Route confusion (batch/proxy forwarding)",
|
|
56216
|
-
severity: "high",
|
|
56217
|
-
evidence: lines(code)[ln - 1]?.trim() ?? "",
|
|
56218
|
-
detail: "Batch/proxy loop forwards to inner handlers with no per-route authorization re-check — an attacker may reach a protected handler."
|
|
56219
|
-
});
|
|
56220
|
-
}
|
|
56221
|
-
}
|
|
56222
|
-
function detectRouteNormalization(code, file, out) {
|
|
56223
|
-
const ls = lines(code);
|
|
56224
|
-
for (let i = 0;i < ls.length; i++) {
|
|
56225
|
-
const l = ls[i];
|
|
56226
|
-
if (!ROUTE_MATCH_RE.test(l))
|
|
56227
|
-
continue;
|
|
56228
|
-
const scope = ls.slice(Math.max(0, i - 3), i + 1).join(`
|
|
56229
|
-
`);
|
|
56230
|
-
if (!userControlled(scope))
|
|
56231
|
-
continue;
|
|
56232
|
-
if (NORMALIZERS.test(l) || NORMALIZERS.test(scope))
|
|
56233
|
-
continue;
|
|
56234
|
-
if (/preg_match\s*\(\s*["'][^"']*\^[^"']*\$[^"']*["']/.test(l))
|
|
56235
|
-
continue;
|
|
56236
|
-
out.push({
|
|
56237
|
-
file,
|
|
56238
|
-
line: i + 1,
|
|
56239
|
-
type: "route_normalization_gap",
|
|
56240
|
-
category: "Route confusion (missing path normalization)",
|
|
56241
|
-
severity: "medium",
|
|
56242
|
-
evidence: l.trim(),
|
|
56243
|
-
detail: "Route matched against user input without visible normalization (strtolower/rtrim/urldecode) — route desync possible."
|
|
56244
|
-
});
|
|
56245
|
-
}
|
|
56246
|
-
}
|
|
56247
|
-
function detectRouteConfusion(code, file) {
|
|
56248
|
-
const out = [];
|
|
56249
|
-
detectDynamicDispatch(code, file, out);
|
|
56250
|
-
detectDynamicMethod(code, file, out);
|
|
56251
|
-
detectDynamicInclude(code, file, out);
|
|
56252
|
-
detectBatchForwarding(code, file, out);
|
|
56253
|
-
detectRouteNormalization(code, file, out);
|
|
56254
|
-
return out;
|
|
56255
|
-
}
|
|
56256
|
-
|
|
56257
|
-
// src/security-state.ts
|
|
56258
|
-
var SANITIZERS2 = {
|
|
56259
|
-
esc_sql: { context: "sql", level: "sanitized" },
|
|
56260
|
-
addslashes: { context: "sql", level: "sanitized" },
|
|
56261
|
-
mysqli_real_escape_string: { context: "sql", level: "sanitized" },
|
|
56262
|
-
mysql_real_escape_string: { context: "sql", level: "sanitized" },
|
|
56263
|
-
pg_escape_string: { context: "sql", level: "sanitized" },
|
|
56264
|
-
sanitize_text_field: { context: "html", level: "sanitized" },
|
|
56265
|
-
sanitize_textarea_field: { context: "html", level: "sanitized" },
|
|
56266
|
-
esc_html: { context: "html", level: "sanitized" },
|
|
56267
|
-
esc_html__: { context: "html", level: "sanitized" },
|
|
56268
|
-
htmlspecialchars: { context: "html", level: "sanitized" },
|
|
56269
|
-
htmlentities: { context: "html", level: "sanitized" },
|
|
56270
|
-
wp_kses: { context: "html", level: "sanitized" },
|
|
56271
|
-
wp_kses_post: { context: "html", level: "sanitized" },
|
|
56272
|
-
wp_kses_data: { context: "html", level: "sanitized" },
|
|
56273
|
-
strip_tags: { context: "html", level: "sanitized" },
|
|
56274
|
-
esc_attr: { context: "html_attr", level: "sanitized" },
|
|
56275
|
-
esc_url: { context: "url", level: "sanitized" },
|
|
56276
|
-
esc_url_raw: { context: "url", level: "sanitized" },
|
|
56277
|
-
esc_textarea: { context: "html", level: "sanitized" },
|
|
56278
|
-
esc_js: { context: "js", level: "sanitized" },
|
|
56279
|
-
urlencode: { context: "url", level: "sanitized" },
|
|
56280
|
-
rawurlencode: { context: "url", level: "sanitized" },
|
|
56281
|
-
wp_json_encode: { context: "json", level: "sanitized" },
|
|
56282
|
-
json_encode: { context: "json", level: "sanitized" },
|
|
56283
|
-
absint: { context: "int", level: "validated" },
|
|
56284
|
-
intval: { context: "int", level: "validated" },
|
|
56285
|
-
sanitize_key: { context: "key", level: "sanitized" },
|
|
56286
|
-
sanitize_email: { context: "email", level: "validated" },
|
|
56287
|
-
sanitize_file_name: { context: "filename", level: "validated" },
|
|
56288
|
-
sanitize_url: { context: "url", level: "validated" },
|
|
56289
|
-
sanitize_title: { context: "slug", level: "validated" },
|
|
56290
|
-
sanitize_title_with_dashes: { context: "slug", level: "validated" },
|
|
56291
|
-
sanitize_user: { context: "username", level: "validated" },
|
|
56292
|
-
filter_var: { context: "validated", level: "validated" },
|
|
56293
|
-
wp_validate_boolean: { context: "validated", level: "validated" }
|
|
56294
|
-
};
|
|
56295
|
-
var PSEUDO_SANITIZERS = new Set([
|
|
56296
|
-
"trim",
|
|
56297
|
-
"ltrim",
|
|
56298
|
-
"rtrim",
|
|
56299
|
-
"strtolower",
|
|
56300
|
-
"strtoupper",
|
|
56301
|
-
"ucfirst",
|
|
56302
|
-
"ucwords",
|
|
56303
|
-
"base64_encode",
|
|
56304
|
-
"base64_decode",
|
|
56305
|
-
"urldecode",
|
|
56306
|
-
"rawurldecode",
|
|
56307
|
-
"stripslashes",
|
|
56308
|
-
"html_entity_decode",
|
|
56309
|
-
"htmlspecialchars_decode",
|
|
56310
|
-
"json_decode",
|
|
56311
|
-
"unserialize",
|
|
56312
|
-
"serialize",
|
|
56313
|
-
"substr",
|
|
56314
|
-
"mb_substr",
|
|
56315
|
-
"explode",
|
|
56316
|
-
"implode",
|
|
56317
|
-
"str_replace",
|
|
56318
|
-
"preg_replace",
|
|
56319
|
-
"nl2br",
|
|
56320
|
-
"strval",
|
|
56321
|
-
"number_format",
|
|
56322
|
-
"sprintf",
|
|
56323
|
-
"md5",
|
|
56324
|
-
"sha1",
|
|
56325
|
-
"hash",
|
|
56326
|
-
"crypt",
|
|
56327
|
-
"chr",
|
|
56328
|
-
"ord",
|
|
56329
|
-
"strrev",
|
|
56330
|
-
"str_repeat",
|
|
56331
|
-
"str_pad"
|
|
56332
|
-
]);
|
|
56333
|
-
var SINK_REQUIREMENTS = {
|
|
56334
|
-
sql_execution: ["sql", "sql_param", "int"],
|
|
56335
|
-
code_execution: ["code"],
|
|
56336
|
-
command_execution: ["code"],
|
|
56337
|
-
deserialization: ["code"],
|
|
56338
|
-
file_operations: ["filename"],
|
|
56339
|
-
redirect: ["url"],
|
|
56340
|
-
html_render: ["html"]
|
|
56341
|
-
};
|
|
56342
|
-
var SOURCE_TOKENS2 = /\$_(GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|->(input|query|request)\(|\breq\.(query|body|params|headers|cookies)|\bgetParameter\(/;
|
|
56343
|
-
function analyzeSecurityState(input) {
|
|
56344
|
-
let state = "tainted";
|
|
56345
|
-
let context = null;
|
|
56346
|
-
for (const fn of input.sanitizers) {
|
|
56347
|
-
const s = SANITIZERS2[fn];
|
|
56348
|
-
if (!s)
|
|
56349
|
-
continue;
|
|
56350
|
-
if (s.level === "validated") {
|
|
56351
|
-
if (state === "tainted" || state === "sanitized") {
|
|
56352
|
-
state = "validated";
|
|
56353
|
-
context = s.context;
|
|
56354
|
-
}
|
|
56355
|
-
} else if (state === "tainted") {
|
|
56356
|
-
state = "sanitized";
|
|
56357
|
-
context = s.context;
|
|
56358
|
-
}
|
|
56359
|
-
}
|
|
56360
|
-
const required = SINK_REQUIREMENTS[input.sinkType] ?? [];
|
|
56361
|
-
if (required.length === 0) {
|
|
56362
|
-
return { verdict: "unknown", state, context, required, reason: `sink type '${input.sinkType}' not in the requirements table` };
|
|
56363
|
-
}
|
|
56364
|
-
if (state === "tainted") {
|
|
56365
|
-
return { verdict: "vulnerable", state, context, required, reason: `raw tainted input reaches a ${input.sinkType} sink with no sanitization` };
|
|
56366
|
-
}
|
|
56367
|
-
if (state === "validated") {
|
|
56368
|
-
return { verdict: "safe", state, context, required, reason: `input validated (${context}) — safe for any sink` };
|
|
56369
|
-
}
|
|
56370
|
-
if (state === "sanitized") {
|
|
56371
|
-
if (context && required.includes(context)) {
|
|
56372
|
-
return { verdict: "safe", state, context, required, reason: `sanitized for '${context}' — matches sink requirement ${required.join("/")}` };
|
|
56373
|
-
}
|
|
56374
|
-
return { verdict: "vulnerable", state, context, required, reason: `sanitized for '${context}' but this sink requires ${required.join("/")} — WRONG sanitizer (context mismatch)` };
|
|
56375
|
-
}
|
|
56376
|
-
return { verdict: "unknown", state, context, required, reason: "unhandled state" };
|
|
56377
|
-
}
|
|
56378
|
-
function sinkTypeOf(line) {
|
|
56379
|
-
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))
|
|
56380
|
-
return "sql_execution";
|
|
56381
|
-
if (/\b(eval|assert)\s*\(/.test(line))
|
|
56382
|
-
return "code_execution";
|
|
56383
|
-
if (/\b(system|exec|shell_exec|passthru|proc_open|popen)\s*\(/.test(line))
|
|
56384
|
-
return "command_execution";
|
|
56385
|
-
if (/\b(unserialize|maybe_unserialize)\s*\(/.test(line))
|
|
56386
|
-
return "deserialization";
|
|
56387
|
-
if (/\b(file_get_contents|file_put_contents|fopen|readfile|include|require|include_once|require_once)\s*\(/.test(line))
|
|
56388
|
-
return "file_operations";
|
|
56389
|
-
if (/header\s*\(\s*['"]Location/i.test(line))
|
|
56390
|
-
return "redirect";
|
|
56391
|
-
return null;
|
|
56392
|
-
}
|
|
56393
|
-
function detectWrongSanitizer(code, file) {
|
|
56394
|
-
const out = [];
|
|
56395
|
-
const ls = code.split(`
|
|
56396
|
-
`);
|
|
56397
|
-
const varSanitizer = new Map;
|
|
56398
|
-
for (const line of ls) {
|
|
56399
|
-
const m = line.match(/\$(\w+)\s*=\s*([a-zA-Z0-9_]+)\s*\(\s*([^)]*)\s*\)\s*;?/);
|
|
56400
|
-
if (!m)
|
|
56401
|
-
continue;
|
|
56402
|
-
const [, varName, fn, arg] = m;
|
|
56403
|
-
if (!SOURCE_TOKENS2.test(arg))
|
|
56404
|
-
continue;
|
|
56405
|
-
if (SANITIZERS2[fn])
|
|
56406
|
-
varSanitizer.set(varName, fn);
|
|
56407
|
-
else if (PSEUDO_SANITIZERS.has(fn))
|
|
56408
|
-
varSanitizer.set(varName, fn);
|
|
56409
|
-
}
|
|
56410
|
-
for (let i = 0;i < ls.length; i++) {
|
|
56411
|
-
const line = ls[i];
|
|
56412
|
-
const sinkType = sinkTypeOf(line);
|
|
56413
|
-
if (!sinkType)
|
|
56414
|
-
continue;
|
|
56415
|
-
const required = SINK_REQUIREMENTS[sinkType] ?? [];
|
|
56416
|
-
for (const [varName, fn] of varSanitizer) {
|
|
56417
|
-
if (!new RegExp(`\\$${varName}\\b`).test(line))
|
|
56418
|
-
continue;
|
|
56419
|
-
const sanitizer = SANITIZERS2[fn];
|
|
56420
|
-
const context = sanitizer?.context ?? null;
|
|
56421
|
-
if (sanitizer && (sanitizer.level === "validated" || context && required.includes(context)))
|
|
56422
|
-
continue;
|
|
56423
|
-
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.`;
|
|
56424
|
-
out.push({
|
|
56425
|
-
file,
|
|
56426
|
-
line: i + 1,
|
|
56427
|
-
type: "wrong_sanitizer",
|
|
56428
|
-
category: "Wrong-context sanitization / pseudo-sanitizer",
|
|
56429
|
-
severity: "high",
|
|
56430
|
-
evidence: line.trim(),
|
|
56431
|
-
detail
|
|
56432
|
-
});
|
|
56433
|
-
}
|
|
56434
|
-
}
|
|
56435
|
-
const seen = new Set;
|
|
56436
|
-
return out.filter((f) => {
|
|
56437
|
-
const key = `${f.type}:${f.line}`;
|
|
56438
|
-
if (seen.has(key))
|
|
56439
|
-
return false;
|
|
56440
|
-
seen.add(key);
|
|
56441
|
-
return true;
|
|
56442
|
-
});
|
|
56443
|
-
}
|
|
56444
|
-
|
|
56445
|
-
// src/framework-security.ts
|
|
56446
|
-
var FRAMEWORKS = {
|
|
56447
|
-
wordpress: {
|
|
56448
|
-
framework: "wordpress",
|
|
56449
|
-
auth_primitives: ["current_user_can", "current_user_can_edit_post", "is_user_logged_in", "authorize"],
|
|
56450
|
-
nonce_primitives: ["wp_verify_nonce", "check_ajax_referer", "check_admin_referer", "verify_nonce"],
|
|
56451
|
-
entry_point_hooks: ["wp_ajax_", "wp_ajax_nopriv_", "admin_post_", "admin_post_nopriv_", "register_rest_route", "add_shortcode", "init", "rest_api_init"],
|
|
56452
|
-
sanitizers: ["sanitize_text_field", "esc_sql", "esc_html", "esc_attr", "esc_url", "wp_kses", "wp_kses_post", "absint", "sanitize_email", "sanitize_file_name"]
|
|
56453
|
-
},
|
|
56454
|
-
wp: {
|
|
56455
|
-
framework: "wordpress",
|
|
56456
|
-
auth_primitives: ["current_user_can", "is_user_logged_in", "authorize"],
|
|
56457
|
-
nonce_primitives: ["wp_verify_nonce", "check_ajax_referer", "check_admin_referer"],
|
|
56458
|
-
entry_point_hooks: ["wp_ajax_", "wp_ajax_nopriv_", "register_rest_route"],
|
|
56459
|
-
sanitizers: ["sanitize_text_field", "esc_sql", "esc_html", "absint"]
|
|
56460
|
-
},
|
|
56461
|
-
laravel: {
|
|
56462
|
-
framework: "laravel",
|
|
56463
|
-
auth_primitives: ["auth", "authorize", "Gate::", "can:", "->middleware('auth'", "->can(", "$this->authorize", "@can", "@cannot", "policy"],
|
|
56464
|
-
nonce_primitives: ["@csrf", "csrf_field", "VerifyCsrfToken"],
|
|
56465
|
-
entry_point_hooks: ["Route::get", "Route::post", "Route::put", "Route::delete", "Route::resource", "Route::match"],
|
|
56466
|
-
sanitizers: ["e(", "htmlspecialchars", "clean(", "strip_tags"]
|
|
56467
|
-
},
|
|
56468
|
-
django: {
|
|
56469
|
-
framework: "django",
|
|
56470
|
-
auth_primitives: ["login_required", "permission_required", "user_passes_test", "LoginRequiredMixin", "PermissionRequiredMixin", "permission_classes", "IsAuthenticated", "has_permission"],
|
|
56471
|
-
nonce_primitives: ["csrf_protect", "CsrfViewMiddleware", "@csrf"],
|
|
56472
|
-
entry_point_hooks: ["def ", "@api_view", "path(", "url(", "re_path("],
|
|
56473
|
-
sanitizers: ["escape(", "mark_safe", "html.escape"]
|
|
56474
|
-
},
|
|
56475
|
-
flask: {
|
|
56476
|
-
framework: "flask",
|
|
56477
|
-
auth_primitives: ["login_required", "@login_required", "current_user.is_authenticated", "permission_required"],
|
|
56478
|
-
nonce_primitives: ["CSRFProtect", "csrf_token"],
|
|
56479
|
-
entry_point_hooks: ["@app.route", "@blueprint.route", "@bp.route"],
|
|
56480
|
-
sanitizers: ["escape(", "Markup", "html.escape"]
|
|
56481
|
-
},
|
|
56482
|
-
express: {
|
|
56483
|
-
framework: "express",
|
|
56484
|
-
auth_primitives: ["requireAuth", "authenticate", "passport.authenticate", "req.user", "isAuthenticated", "ensureAuthenticated", "requireLogin", "authorize", "authMiddleware"],
|
|
56485
|
-
nonce_primitives: ["csurf", "csrfProtection", "helmet"],
|
|
56486
|
-
entry_point_hooks: ["app.get", "app.post", "router.get", "router.post"],
|
|
56487
|
-
sanitizers: ["escapeHtml", "encodeURIComponent", "sanitize", "xss"]
|
|
56488
|
-
},
|
|
56489
|
-
spring: {
|
|
56490
|
-
framework: "spring",
|
|
56491
|
-
auth_primitives: ["@PreAuthorize", "@Secured", "@RolesAllowed", "hasRole", "hasAuthority", "isAuthenticated", "SecurityContextHolder"],
|
|
56492
|
-
nonce_primitives: ["csrf", "CsrfToken"],
|
|
56493
|
-
entry_point_hooks: ["@GetMapping", "@PostMapping", "@RequestMapping", "@Controller"],
|
|
56494
|
-
sanitizers: ["HtmlUtils.htmlEscape", "escapeHtml", "StringEscapeUtils"]
|
|
56495
|
-
},
|
|
56496
|
-
symfony: {
|
|
56497
|
-
framework: "symfony",
|
|
56498
|
-
auth_primitives: ["is_granted", "denyAccessUnlessGranted", "->isGranted(", "IsGranted", "Voter"],
|
|
56499
|
-
nonce_primitives: ["isCsrfTokenValid", "csrf_token"],
|
|
56500
|
-
entry_point_hooks: ["#[Route", "@Route", "#[Get", "#[Post"],
|
|
56501
|
-
sanitizers: ["e(", "htmlspecialchars", "escape("]
|
|
56502
|
-
},
|
|
56503
|
-
aspnet: {
|
|
56504
|
-
framework: "aspnet",
|
|
56505
|
-
auth_primitives: ["[Authorize", "[AllowAnonymous", "User.Identity", "IsInRole", "AuthorizeAsync", "IAuthorizationService"],
|
|
56506
|
-
nonce_primitives: ["ValidateAntiForgeryToken", "Antiforgery"],
|
|
56507
|
-
entry_point_hooks: ["[HttpGet", "[HttpPost", "[Route("],
|
|
56508
|
-
sanitizers: ["HtmlEncoder", "Html.Raw", "UrlEncoder"]
|
|
56509
|
-
},
|
|
56510
|
-
rails: {
|
|
56511
|
-
framework: "rails",
|
|
56512
|
-
auth_primitives: ["authenticate_user!", "authorize", "before_action :authenticate", "before_action :authorize", "current_user"],
|
|
56513
|
-
nonce_primitives: ["protect_from_forgery", "form_authenticity_token"],
|
|
56514
|
-
entry_point_hooks: ["def ", "before_action"],
|
|
56515
|
-
sanitizers: ["h(", "html_escape", "sanitize"]
|
|
56516
|
-
}
|
|
56517
|
-
};
|
|
56518
|
-
function frameworkKnowledge(framework = "wordpress") {
|
|
56519
|
-
return FRAMEWORKS[framework.toLowerCase()] ?? null;
|
|
56520
|
-
}
|
|
56521
|
-
function listFrameworks2() {
|
|
56522
|
-
const s = new Set;
|
|
56523
|
-
for (const k of Object.keys(FRAMEWORKS))
|
|
56524
|
-
s.add(FRAMEWORKS[k].framework);
|
|
56525
|
-
return [...s];
|
|
56526
|
-
}
|
|
56527
|
-
function lineNoOf(code, pos) {
|
|
56528
|
-
let n = 1;
|
|
56529
|
-
for (let i = 0;i < pos; i++)
|
|
56530
|
-
if (code.charCodeAt(i) === 10)
|
|
56531
|
-
n++;
|
|
56532
|
-
return n;
|
|
56533
|
-
}
|
|
56534
|
-
function functionBody(code, name) {
|
|
56535
|
-
const idx = code.search(new RegExp(`function\\s+${name}\\s*\\(`));
|
|
56536
|
-
if (idx === -1)
|
|
56537
|
-
return null;
|
|
56538
|
-
const open = code.indexOf("{", idx);
|
|
56539
|
-
if (open === -1)
|
|
56540
|
-
return null;
|
|
56541
|
-
let depth = 0;
|
|
56542
|
-
for (let i = open;i < code.length; i++) {
|
|
56543
|
-
if (code[i] === "{")
|
|
56544
|
-
depth++;
|
|
56545
|
-
else if (code[i] === "}") {
|
|
56546
|
-
depth--;
|
|
56547
|
-
if (depth === 0)
|
|
56548
|
-
return code.slice(open, i + 1);
|
|
56549
|
-
}
|
|
56550
|
-
}
|
|
56551
|
-
return code.slice(open);
|
|
56552
|
-
}
|
|
56553
|
-
function handlerName(expr) {
|
|
56554
|
-
const quoted = expr.match(/['"]([A-Za-z_][A-Za-z0-9_]*)['"]/g);
|
|
56555
|
-
if (quoted && quoted.length > 0)
|
|
56556
|
-
return quoted[quoted.length - 1].replace(/['"]/g, "");
|
|
56557
|
-
return null;
|
|
56558
|
-
}
|
|
56559
|
-
var INDIRECT_AUTH = /\b[a-zA-Z0-9_]*(auth|permission|cap|nonce|can_|check|verify|allow)[a-zA-Z0-9_]*\s*\(/;
|
|
56560
|
-
var AUTH_RE = /current_user_can|is_user_logged_in|is_super_admin|current_user_can_edit_post|authorize/;
|
|
56561
|
-
var NONCE_RE = /wp_verify_nonce|check_ajax_referer|check_admin_referer|verify_nonce/;
|
|
56562
|
-
var DANGEROUS_OP_RE = /\b(update_option|add_option|delete_option|update_site_option|delete_site_option|update_post_meta|add_post_meta|delete_post_meta|update_user_meta|add_user_meta|delete_user_meta|update_metadata|add_metadata|delete_metadata|set_transient|delete_transient|wp_insert_post|wp_update_post|wp_delete_post|wp_trash_post|wp_untrash_post|wp_insert_comment|wp_update_comment|wp_delete_comment|wp_insert_user|wp_update_user|wp_delete_user|wp_create_user|wp_set_current_user|wp_set_auth_cookie|set_user_role|set_role|add_role|remove_role|add_cap|remove_cap|wp_mail|wp_new_user_notification|wp_handle_upload|move_uploaded_file|wp_upload_bits|wp_set_object_terms|wp_set_post_terms|wp_schedule_event|wp_clear_scheduled_hook|wp_redirect|file_put_contents|unlink|rename|wpdb->query|wpdb->insert|wpdb->update|wpdb->delete|wpdb->replace)\s*\(/;
|
|
56563
|
-
function detectWpAuthz(code, file, out) {
|
|
56564
|
-
const ajaxRe = /add_action\s*\(\s*['"](wp_ajax(?:_nopriv)?_\w+)['"]\s*,\s*(.+?)\s*\)/g;
|
|
56565
|
-
let m;
|
|
56566
|
-
while ((m = ajaxRe.exec(code)) !== null) {
|
|
56567
|
-
const hook = m[1];
|
|
56568
|
-
const handler = handlerName(m[2]);
|
|
56569
|
-
if (!handler)
|
|
56570
|
-
continue;
|
|
56571
|
-
const nopriv = hook.startsWith("wp_ajax_nopriv_");
|
|
56572
|
-
const body = functionBody(code, handler);
|
|
56573
|
-
if (!body)
|
|
56574
|
-
continue;
|
|
56575
|
-
const hasCap = AUTH_RE.test(body) || INDIRECT_AUTH.test(body);
|
|
56576
|
-
const hasNonce = NONCE_RE.test(body);
|
|
56577
|
-
const dangerous = DANGEROUS_OP_RE.test(body);
|
|
56578
|
-
const line = lineNoOf(code, m.index);
|
|
56579
|
-
if (nopriv && !hasCap && dangerous) {
|
|
56580
|
-
out.push({ file, line, type: "missing_authz", category: "Missing capability check (unauthenticated AJAX handler)", severity: "high", evidence: `add_action('${hook}', ...)`, detail: `Unauthenticated AJAX handler '${handler}()' performs a privileged/state-changing operation with no current_user_can()/authorization check — auth bypass / privilege escalation.` });
|
|
56581
|
-
}
|
|
56582
|
-
if (!hasNonce && dangerous) {
|
|
56583
|
-
out.push({ file, line, type: "missing_nonce", category: "Missing nonce verification", severity: "medium", evidence: `add_action('${hook}', ...)`, detail: `AJAX handler '${handler}()' performs a state-changing operation without nonce verification (wp_verify_nonce / check_ajax_referer) — CSRF.` });
|
|
56584
|
-
}
|
|
56585
|
-
}
|
|
56586
|
-
const adminRe = /add_action\s*\(\s*['"](admin_post(?:_nopriv)?_\w+)['"]\s*,\s*(.+?)\s*\)/g;
|
|
56587
|
-
while ((m = adminRe.exec(code)) !== null) {
|
|
56588
|
-
const hook = m[1];
|
|
56589
|
-
const handler = handlerName(m[2]);
|
|
56590
|
-
if (!handler)
|
|
56591
|
-
continue;
|
|
56592
|
-
const nopriv = hook.startsWith("admin_post_nopriv_");
|
|
56593
|
-
const body = functionBody(code, handler);
|
|
56594
|
-
if (!body)
|
|
56595
|
-
continue;
|
|
56596
|
-
const hasCap = AUTH_RE.test(body) || INDIRECT_AUTH.test(body);
|
|
56597
|
-
const hasNonce = NONCE_RE.test(body);
|
|
56598
|
-
const dangerous = DANGEROUS_OP_RE.test(body);
|
|
56599
|
-
const line = lineNoOf(code, m.index);
|
|
56600
|
-
if (nopriv && !hasCap && dangerous) {
|
|
56601
|
-
out.push({ file, line, type: "missing_authz", category: "Missing capability check (unauthenticated admin-post handler)", severity: "high", evidence: `add_action('${hook}', ...)`, detail: `Unauthenticated admin-post handler '${handler}()' performs a privileged/state-changing operation with no authorization check — auth bypass / privilege escalation.` });
|
|
56602
|
-
}
|
|
56603
|
-
if (!hasNonce && dangerous) {
|
|
56604
|
-
out.push({ file, line, type: "missing_nonce", category: "Missing nonce verification", severity: "medium", evidence: `add_action('${hook}', ...)`, detail: `admin-post handler '${handler}()' performs a state-changing operation without nonce verification — CSRF.` });
|
|
56605
|
-
}
|
|
56606
|
-
}
|
|
56607
|
-
const restRe = /register_rest_route\s*\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]/g;
|
|
56608
|
-
while ((m = restRe.exec(code)) !== null) {
|
|
56609
|
-
const line = lineNoOf(code, m.index);
|
|
56610
|
-
const window2 = code.slice(m.index, m.index + 800);
|
|
56611
|
-
const pc = window2.match(/permission_callback['"]?\s*=>\s*(.+?)(?:,|\s*\n|\])/);
|
|
56612
|
-
if (pc) {
|
|
56613
|
-
const expr = pc[1].trim();
|
|
56614
|
-
if (/__return_true/.test(expr)) {
|
|
56615
|
-
out.push({ file, line, type: "missing_authz", category: "REST route explicitly public (__return_true)", severity: "medium", evidence: `register_rest_route('${m[1]}', '${m[2]}')`, detail: `REST route '${m[1]}${m[2]}' sets permission_callback to __return_true (always allow) — verify the endpoint does not expose sensitive data or perform privileged actions.` });
|
|
56616
|
-
}
|
|
56617
|
-
continue;
|
|
56618
|
-
}
|
|
56619
|
-
out.push({ file, line, type: "missing_authz", category: "REST route missing permission_callback", severity: "high", evidence: `register_rest_route('${m[1]}', '${m[2]}')`, detail: `REST route '${m[1]}${m[2]}' has no permission_callback — the endpoint defaults to public (auth bypass).` });
|
|
56620
|
-
}
|
|
56621
|
-
const formRe = /add_action\s*\(\s*['"](init|admin_init|wp_loaded|template_redirect|parse_request|wp)['"]\s*,\s*(.+?)\s*\)/g;
|
|
56622
|
-
while ((m = formRe.exec(code)) !== null) {
|
|
56623
|
-
const hook = m[1];
|
|
56624
|
-
const handler = handlerName(m[2]);
|
|
56625
|
-
if (!handler)
|
|
56626
|
-
continue;
|
|
56627
|
-
const body = functionBody(code, handler);
|
|
56628
|
-
if (!body)
|
|
56629
|
-
continue;
|
|
56630
|
-
if (!/\$_(GET|POST|REQUEST|COOKIE|SERVER)\b|php:\/\/input|filter_input|file_get_contents\(['"]php:\/\/input/.test(body))
|
|
56631
|
-
continue;
|
|
56632
|
-
const hasCap = AUTH_RE.test(body) || INDIRECT_AUTH.test(body);
|
|
56633
|
-
const hasNonce = NONCE_RE.test(body);
|
|
56634
|
-
const dangerous = DANGEROUS_OP_RE.test(body);
|
|
56635
|
-
const line = lineNoOf(code, m.index);
|
|
56636
|
-
if (!hasNonce && dangerous) {
|
|
56637
|
-
out.push({ file, line, type: "missing_nonce", category: "Missing nonce verification (direct request handler)", severity: "medium", evidence: `add_action('${hook}', ...)`, detail: `Request handler '${handler}()' (on '${hook}') performs a state-changing operation without nonce verification — CSRF.` });
|
|
56638
|
-
}
|
|
56639
|
-
if (!hasCap && dangerous) {
|
|
56640
|
-
out.push({ file, line, type: "missing_authz", category: "Missing capability check (direct request handler)", severity: "high", evidence: `add_action('${hook}', ...)`, detail: `Request handler '${handler}()' (on '${hook}') performs a privileged/state-changing operation with no capability check — auth bypass / privilege escalation.` });
|
|
56641
|
-
}
|
|
56642
|
-
}
|
|
56643
|
-
}
|
|
56644
|
-
var ROUTE_DETECTORS = [
|
|
56645
|
-
{ framework: "laravel", entry: /Route::(?:get|post|put|patch|delete|any|match|resource)\s*\(\s*['"][^'"]+['"]/g, auth: /->middleware\s*\(\s*['"]auth|middleware\s*\(\s*\[[^\]]*['"]auth|->can\s*\(|\$this->authorize|Gate::|authorize\s*\(|@can\b/, window: 2 },
|
|
56646
|
-
{ framework: "django", entry: /@api_view\s*\(|def\s+\w+\s*\(\s*request\s*[,)]/g, auth: /login_required|permission_required|user_passes_test|LoginRequiredMixin|PermissionRequiredMixin|permission_classes|IsAuthenticated|has_permission/, window: 3 },
|
|
56647
|
-
{ framework: "flask", entry: /@(?:app|bp|blueprint)\.route\s*\(/g, auth: /login_required|current_user\.is_authenticated|permission_required/, window: 3 },
|
|
56648
|
-
{ framework: "express", entry: /(?:app|router)\.(?:get|post|put|delete|patch|use)\s*\(\s*['"][^'"]+['"]/g, auth: /requireAuth|authenticate|passport\.authenticate|req\.user|isAuthenticated|ensureAuthenticated|requireLogin|\.authorize|authMiddleware/, window: 2 },
|
|
56649
|
-
{ framework: "spring", entry: /@(?:Get|Post|Put|Delete|Patch|Request)Mapping\s*\(/g, auth: /@PreAuthorize|@Secured|@RolesAllowed|hasRole|hasAuthority|isAuthenticated|SecurityContextHolder/, window: 2 },
|
|
56650
|
-
{ framework: "symfony", entry: /#\[(?:Route|Get|Post|Put|Delete|Patch)\b|@Route\s*\(/g, auth: /is_granted|denyAccessUnlessGranted|->isGranted\s*\(|IsGranted|Voter/, window: 2 },
|
|
56651
|
-
{ framework: "aspnet", entry: /\[Http(?:Get|Post|Put|Delete|Patch)\b/g, auth: /\[Authorize|User\.Identity|IsInRole|AuthorizeAsync|\[AllowAnonymous|IAuthorizationService/, window: 2 },
|
|
56652
|
-
{ framework: "rails", entry: /class\s+\w+\s*<\s*ApplicationController/g, auth: /before_action\s+:authenticate|before_action\s+:authorize|authenticate_user!|authorize\s+|current_user|authenticate/, window: 6 }
|
|
56653
|
-
];
|
|
56654
|
-
function detectRouteAuthz(code, file, out) {
|
|
56655
|
-
const ls = code.split(`
|
|
56656
|
-
`);
|
|
56657
|
-
for (const d of ROUTE_DETECTORS) {
|
|
56658
|
-
d.entry.lastIndex = 0;
|
|
56659
|
-
let m;
|
|
56660
|
-
while ((m = d.entry.exec(code)) !== null) {
|
|
56661
|
-
const ln = lineNoOf(code, m.index);
|
|
56662
|
-
const window2 = ls.slice(Math.max(0, ln - d.window), Math.min(ls.length, ln + d.window + 1)).join(`
|
|
56663
|
-
`);
|
|
56664
|
-
if (d.auth.test(window2))
|
|
56665
|
-
continue;
|
|
56666
|
-
out.push({
|
|
56667
|
-
file,
|
|
56668
|
-
line: ln,
|
|
56669
|
-
type: "missing_authz",
|
|
56670
|
-
category: `${d.framework}: endpoint without authorization`,
|
|
56671
|
-
severity: "high",
|
|
56672
|
-
evidence: (ls[ln - 1] ?? "").trim(),
|
|
56673
|
-
detail: `${d.framework} endpoint has no authorization enforcement in the surrounding ${d.window}-line window — auth bypass / privilege escalation.`
|
|
56674
|
-
});
|
|
56675
|
-
}
|
|
57285
|
+
case "get_cookie":
|
|
57286
|
+
return browserGetCookie(args[0] ?? "");
|
|
57287
|
+
case "set_cookie":
|
|
57288
|
+
return browserSetCookie(args[0] ?? "", args[1] ?? "");
|
|
56676
57289
|
}
|
|
56677
57290
|
}
|
|
56678
|
-
function
|
|
56679
|
-
|
|
56680
|
-
detectWpAuthz(code, file, out);
|
|
56681
|
-
detectRouteAuthz(code, file, out);
|
|
56682
|
-
const seen = new Set;
|
|
56683
|
-
return out.filter((f) => {
|
|
56684
|
-
const key = `${f.type}:${f.line}`;
|
|
56685
|
-
if (seen.has(key))
|
|
56686
|
-
return false;
|
|
56687
|
-
seen.add(key);
|
|
56688
|
-
return true;
|
|
56689
|
-
});
|
|
57291
|
+
function browserDiagnostics() {
|
|
57292
|
+
return { console_errors: [...consoleErrors], page_errors: [...pageErrors] };
|
|
56690
57293
|
}
|
|
56691
57294
|
|
|
56692
|
-
// src/
|
|
56693
|
-
|
|
56694
|
-
|
|
57295
|
+
// src/server.ts
|
|
57296
|
+
init_mcp_status();
|
|
57297
|
+
|
|
57298
|
+
// src/route-confusion.ts
|
|
57299
|
+
var SOURCE_TOKENS = /\$_(GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|\$request->|\$req->|request\.(get|input|query|post)|getParameter\(|req\.(query|body|params)/;
|
|
57300
|
+
function lines(code) {
|
|
56695
57301
|
return code.split(`
|
|
56696
57302
|
`);
|
|
56697
57303
|
}
|
|
56698
|
-
function
|
|
57304
|
+
function lineNo(code, pos) {
|
|
56699
57305
|
let n = 1;
|
|
56700
57306
|
for (let i = 0;i < pos; i++)
|
|
56701
57307
|
if (code.charCodeAt(i) === 10)
|
|
56702
57308
|
n++;
|
|
56703
57309
|
return n;
|
|
56704
57310
|
}
|
|
56705
|
-
function
|
|
56706
|
-
|
|
56707
|
-
const n = code.length;
|
|
56708
|
-
let i = 0;
|
|
56709
|
-
while (i < n) {
|
|
56710
|
-
const c = code[i];
|
|
56711
|
-
const nc = code[i + 1];
|
|
56712
|
-
if (c === "'" || c === '"') {
|
|
56713
|
-
const q = c;
|
|
56714
|
-
i++;
|
|
56715
|
-
while (i < n && code[i] !== q) {
|
|
56716
|
-
if (code[i] === "\\")
|
|
56717
|
-
i++;
|
|
56718
|
-
i++;
|
|
56719
|
-
}
|
|
56720
|
-
i++;
|
|
56721
|
-
continue;
|
|
56722
|
-
}
|
|
56723
|
-
if (c === "/" && nc === "/" || c === "#") {
|
|
56724
|
-
while (i < n && code[i] !== `
|
|
56725
|
-
`) {
|
|
56726
|
-
out[i] = " ";
|
|
56727
|
-
i++;
|
|
56728
|
-
}
|
|
56729
|
-
continue;
|
|
56730
|
-
}
|
|
56731
|
-
if (c === "/" && nc === "*") {
|
|
56732
|
-
out[i] = " ";
|
|
56733
|
-
out[i + 1] = " ";
|
|
56734
|
-
i += 2;
|
|
56735
|
-
while (i < n && !(code[i] === "*" && code[i + 1] === "/")) {
|
|
56736
|
-
if (code[i] !== `
|
|
56737
|
-
`)
|
|
56738
|
-
out[i] = " ";
|
|
56739
|
-
i++;
|
|
56740
|
-
}
|
|
56741
|
-
if (i < n) {
|
|
56742
|
-
out[i] = " ";
|
|
56743
|
-
if (i + 1 < n)
|
|
56744
|
-
out[i + 1] = " ";
|
|
56745
|
-
i += 2;
|
|
56746
|
-
}
|
|
56747
|
-
continue;
|
|
56748
|
-
}
|
|
56749
|
-
i++;
|
|
56750
|
-
}
|
|
56751
|
-
return out.join("");
|
|
56752
|
-
}
|
|
56753
|
-
var MAGIC_METHODS = /\b(__destruct|__wakeup|__toString|__invoke|__call|__get|__set|__isset|__unset|__sleep)\s*\(/;
|
|
56754
|
-
function detectDeserialization(code, file, out) {
|
|
56755
|
-
if (!MAGIC_METHODS.test(code))
|
|
56756
|
-
return;
|
|
56757
|
-
const ls = lines2(code);
|
|
56758
|
-
const re = /\b(unserialize|maybe_unserialize)\s*\(/g;
|
|
56759
|
-
let m;
|
|
56760
|
-
while ((m = re.exec(code)) !== null) {
|
|
56761
|
-
const ln = lineNo2(code, m.index);
|
|
56762
|
-
const l = ls[ln - 1] ?? "";
|
|
56763
|
-
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
56764
|
-
`);
|
|
56765
|
-
if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
|
|
56766
|
-
continue;
|
|
56767
|
-
out.push({
|
|
56768
|
-
file,
|
|
56769
|
-
line: ln,
|
|
56770
|
-
type: "deserialization",
|
|
56771
|
-
category: "Deserialization → POP gadget chain",
|
|
56772
|
-
severity: "high",
|
|
56773
|
-
evidence: l.trim(),
|
|
56774
|
-
detail: `\`${m[1]}()\` of attacker-controlled data AND a magic method is present — object injection can pivot to RCE via the gadget chain.`
|
|
56775
|
-
});
|
|
56776
|
-
}
|
|
57311
|
+
function userControlled(scope) {
|
|
57312
|
+
return SOURCE_TOKENS.test(scope);
|
|
56777
57313
|
}
|
|
56778
|
-
|
|
56779
|
-
|
|
56780
|
-
|
|
57314
|
+
var DYNAMIC_DISPATCH_RE = /\b(call_user_func|call_user_func_array|forward|dispatch|resolve_route|invoke)\s*\(\s*(\$|\[)/gi;
|
|
57315
|
+
var DYNAMIC_METHOD_RE = /(->|::)\s*\$[A-Za-z_]/g;
|
|
57316
|
+
var BATCH_LOOP_RE = /\bforeach\s*\([^)]*(requests|batch|sub_requests|operations)\b/gi;
|
|
57317
|
+
var ROUTE_MATCH_RE = /\b(preg_match|preg_match_all|strpos|stripos|str_contains)\s*\(/i;
|
|
57318
|
+
var NORMALIZERS = /\b(strtolower|strtoupper|rtrim|urldecode|rawurldecode|parse_url|normalize|canonical)\b/i;
|
|
57319
|
+
var WHITELIST_LOOKUP = /\$[A-Za-z_]\w*\s*\[\s*\$[A-Za-z_]\w*\s*\]/;
|
|
57320
|
+
var CANONICALIZE = /\b(basename|realpath|normalize|canonical|resolve|toRealPath)\s*\(/i;
|
|
57321
|
+
function detectDynamicDispatch(code, file, out) {
|
|
57322
|
+
const ls = lines(code);
|
|
57323
|
+
DYNAMIC_DISPATCH_RE.lastIndex = 0;
|
|
56781
57324
|
let m;
|
|
56782
|
-
while ((m =
|
|
56783
|
-
const ln =
|
|
57325
|
+
while ((m = DYNAMIC_DISPATCH_RE.exec(code)) !== null) {
|
|
57326
|
+
const ln = lineNo(code, m.index);
|
|
56784
57327
|
const l = ls[ln - 1] ?? "";
|
|
56785
57328
|
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
56786
57329
|
`);
|
|
56787
|
-
|
|
56788
|
-
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);
|
|
56789
|
-
if (!hasSource || !hasHash)
|
|
57330
|
+
if (!userControlled(l) && !userControlled(scope))
|
|
56790
57331
|
continue;
|
|
56791
|
-
|
|
56792
|
-
file,
|
|
56793
|
-
line: ln,
|
|
56794
|
-
type: "type_juggling",
|
|
56795
|
-
category: "Type juggling (loose comparison)",
|
|
56796
|
-
severity: "medium",
|
|
56797
|
-
evidence: l.trim(),
|
|
56798
|
-
detail: 'Loose comparison of user input against a hash/secret — PHP type juggling (`0e…`, `"str" == 0`) may bypass the check. Use strict `===`.'
|
|
56799
|
-
});
|
|
56800
|
-
}
|
|
56801
|
-
}
|
|
56802
|
-
function detectMassAssignment(code, file, out) {
|
|
56803
|
-
const ls = lines2(code);
|
|
56804
|
-
const extractRe = /\bextract\s*\(\s*[^)]*(REQUEST|POST|GET|COOKIE|FILES|\$_(REQUEST|POST|GET|COOKIE|FILES))/g;
|
|
56805
|
-
let m;
|
|
56806
|
-
while ((m = extractRe.exec(code)) !== null) {
|
|
56807
|
-
const ln = lineNo2(code, m.index);
|
|
56808
|
-
const l = ls[ln - 1] ?? "";
|
|
56809
|
-
if (/EXTR_SKIP|EXTR_PREFIX_ALL/.test(l))
|
|
57332
|
+
if (WHITELIST_LOOKUP.test(l) || WHITELIST_LOOKUP.test(scope))
|
|
56810
57333
|
continue;
|
|
56811
57334
|
out.push({
|
|
56812
57335
|
file,
|
|
56813
57336
|
line: ln,
|
|
56814
|
-
type: "
|
|
56815
|
-
category: "
|
|
57337
|
+
type: "dynamic_dispatch",
|
|
57338
|
+
category: "Route confusion (user-controlled dispatch)",
|
|
56816
57339
|
severity: "high",
|
|
56817
57340
|
evidence: l.trim(),
|
|
56818
|
-
detail:
|
|
56819
|
-
});
|
|
56820
|
-
}
|
|
56821
|
-
const parseRe = /\bparse_str\s*\(/g;
|
|
56822
|
-
while ((m = parseRe.exec(code)) !== null) {
|
|
56823
|
-
const ln = lineNo2(code, m.index);
|
|
56824
|
-
const l = ls[ln - 1] ?? "";
|
|
56825
|
-
const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
|
|
56826
|
-
`);
|
|
56827
|
-
if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
|
|
56828
|
-
continue;
|
|
56829
|
-
out.push({
|
|
56830
|
-
file,
|
|
56831
|
-
line: ln,
|
|
56832
|
-
type: "mass_assignment",
|
|
56833
|
-
category: "Mass assignment (variable injection)",
|
|
56834
|
-
severity: "medium",
|
|
56835
|
-
evidence: l.trim(),
|
|
56836
|
-
detail: "`parse_str()` of user input into the symbol table — variable injection."
|
|
57341
|
+
detail: `\`${m[1]}()\` invoked with an attacker-controlled callback/route — the request selects the handler.`
|
|
56837
57342
|
});
|
|
56838
57343
|
}
|
|
56839
57344
|
}
|
|
56840
|
-
function
|
|
56841
|
-
const
|
|
56842
|
-
|
|
56843
|
-
detectTypeJuggling(code, file, out);
|
|
56844
|
-
detectMassAssignment(code, file, out);
|
|
56845
|
-
detectPrototypePollution(code, file, out);
|
|
56846
|
-
detectCrlfInjection(code, file, out);
|
|
56847
|
-
detectPathConfusion(code, file, out);
|
|
56848
|
-
detectSsrF(code, file, out);
|
|
56849
|
-
detectXxe(code, file, out);
|
|
56850
|
-
detectSsti(code, file, out);
|
|
56851
|
-
detectFileUploadRce(code, file, out);
|
|
56852
|
-
detectUnrestrictedUpload(code, file, out);
|
|
56853
|
-
detectSqlInjection(code, file, out);
|
|
56854
|
-
detectHardcodedSecret(code, file, out);
|
|
56855
|
-
out.push(...detectWrongSanitizer(code, file));
|
|
56856
|
-
out.push(...detectMissingAuthz(code, file));
|
|
56857
|
-
const seen = new Set;
|
|
56858
|
-
return out.filter((f) => {
|
|
56859
|
-
const key = `${f.type}:${f.line}`;
|
|
56860
|
-
if (seen.has(key))
|
|
56861
|
-
return false;
|
|
56862
|
-
seen.add(key);
|
|
56863
|
-
return true;
|
|
56864
|
-
});
|
|
56865
|
-
}
|
|
56866
|
-
function detectPrototypePollution(code, file, out) {
|
|
56867
|
-
const ls = lines2(code);
|
|
56868
|
-
const mergeRe = /\b(Object\.assign|_\.merge|_\.defaultsDeep|\.extend|\.merge|\.defaultsDeep|mergeDeep|deepMerge|_.set)\s*\(/g;
|
|
57345
|
+
function detectDynamicMethod(code, file, out) {
|
|
57346
|
+
const ls = lines(code);
|
|
57347
|
+
DYNAMIC_METHOD_RE.lastIndex = 0;
|
|
56869
57348
|
let m;
|
|
56870
|
-
while ((m =
|
|
56871
|
-
const ln =
|
|
57349
|
+
while ((m = DYNAMIC_METHOD_RE.exec(code)) !== null) {
|
|
57350
|
+
const ln = lineNo(code, m.index);
|
|
56872
57351
|
const l = ls[ln - 1] ?? "";
|
|
56873
57352
|
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
56874
57353
|
`);
|
|
56875
|
-
if (!
|
|
56876
|
-
continue;
|
|
56877
|
-
out.push({
|
|
56878
|
-
file,
|
|
56879
|
-
line: ln,
|
|
56880
|
-
type: "prototype_pollution",
|
|
56881
|
-
category: "Prototype pollution (unsafe merge)",
|
|
56882
|
-
severity: "high",
|
|
56883
|
-
evidence: l.trim(),
|
|
56884
|
-
detail: "Merge/extend of request data into an object without a `__proto__`/`constructor.prototype` guard — attacker-controlled keys can pollute `Object.prototype` and pivot to RCE or property injection."
|
|
56885
|
-
});
|
|
56886
|
-
}
|
|
56887
|
-
}
|
|
56888
|
-
function detectCrlfInjection(code, file, out) {
|
|
56889
|
-
const ls = lines2(code);
|
|
56890
|
-
const headerRe = /\b(header|setHeader|addHeader|writeHead|appendHeader)\s*\(/g;
|
|
56891
|
-
let m;
|
|
56892
|
-
while ((m = headerRe.exec(code)) !== null) {
|
|
56893
|
-
const ln = lineNo2(code, m.index);
|
|
56894
|
-
const l = ls[ln - 1] ?? "";
|
|
56895
|
-
const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
|
|
56896
|
-
`);
|
|
56897
|
-
if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
|
|
56898
|
-
continue;
|
|
56899
|
-
out.push({
|
|
56900
|
-
file,
|
|
56901
|
-
line: ln,
|
|
56902
|
-
type: "crlf_injection",
|
|
56903
|
-
category: "CRLF / header injection",
|
|
56904
|
-
severity: "medium",
|
|
56905
|
-
evidence: l.trim(),
|
|
56906
|
-
detail: "User input reaches an HTTP header — CRLF (`%0d%0a`) can split the response, inject headers, or poison cookies. Strip newlines from header values."
|
|
56907
|
-
});
|
|
56908
|
-
}
|
|
56909
|
-
}
|
|
56910
|
-
function detectPathConfusion(code, file, out) {
|
|
56911
|
-
const ls = lines2(code);
|
|
56912
|
-
const fileRe = /\b(file_get_contents|file_put_contents|fopen|readFileSync|readFile|open|include|require|new\s+File|Files\.read|FileInputStream|FileOutputStream)\s*\(/g;
|
|
56913
|
-
let m;
|
|
56914
|
-
while ((m = fileRe.exec(code)) !== null) {
|
|
56915
|
-
const ln = lineNo2(code, m.index);
|
|
56916
|
-
const l = ls[ln - 1] ?? "";
|
|
56917
|
-
const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
|
|
56918
|
-
`);
|
|
56919
|
-
if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
|
|
56920
|
-
continue;
|
|
56921
|
-
if (/\b(basename|realpath|canonicalPath|getCanonicalPath|normalize|resolve|toRealPath)\s*\(/i.test(scope + `
|
|
56922
|
-
` + l))
|
|
56923
|
-
continue;
|
|
56924
|
-
if (/\$[A-Za-z_]\w*\s*\[\s*\$[A-Za-z_]\w*\s*\]/.test(scope + `
|
|
56925
|
-
` + l))
|
|
56926
|
-
continue;
|
|
56927
|
-
if (/["'][^"']*:\/\/[^"']*["']/.test(scope + `
|
|
56928
|
-
` + l))
|
|
56929
|
-
continue;
|
|
56930
|
-
out.push({
|
|
56931
|
-
file,
|
|
56932
|
-
line: ln,
|
|
56933
|
-
type: "path_confusion",
|
|
56934
|
-
category: "Path confusion (unsanitized file path)",
|
|
56935
|
-
severity: "medium",
|
|
56936
|
-
evidence: l.trim(),
|
|
56937
|
-
detail: "User input reaches a file path with no canonicalization (`basename`/`realpath`/`normalize`) — traversal, `..%2f`, backslash and null-byte variants can confuse a naive `../` filter."
|
|
56938
|
-
});
|
|
56939
|
-
}
|
|
56940
|
-
}
|
|
56941
|
-
function detectSsrF(code, file, out) {
|
|
56942
|
-
const ls = lines2(code);
|
|
56943
|
-
const re = /\b(file_get_contents|fopen|file|readfile|curl_setopt|curl_init|fetch|axios|requests\.(get|post|put)|urllib\.request|http_get|wp_remote_(get|post)|new\s+URL\s*\(|got\(|http\.(get|request))\s*\(/g;
|
|
56944
|
-
let m;
|
|
56945
|
-
while ((m = re.exec(code)) !== null) {
|
|
56946
|
-
const ln = lineNo2(code, m.index);
|
|
56947
|
-
const l = ls[ln - 1] ?? "";
|
|
56948
|
-
const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
|
|
56949
|
-
`);
|
|
56950
|
-
if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
|
|
56951
|
-
continue;
|
|
56952
|
-
if (/(allowlist|whitelist|in_array|preg_match|parse_url|startsWith\(["']https?|endsWith\(["']|basename|realpath|normalize)/i.test(scope + `
|
|
56953
|
-
` + l))
|
|
57354
|
+
if (!userControlled(l) && !userControlled(scope))
|
|
56954
57355
|
continue;
|
|
56955
|
-
if (
|
|
56956
|
-
` + l))
|
|
57356
|
+
if (WHITELIST_LOOKUP.test(l) || WHITELIST_LOOKUP.test(scope))
|
|
56957
57357
|
continue;
|
|
56958
57358
|
out.push({
|
|
56959
57359
|
file,
|
|
56960
57360
|
line: ln,
|
|
56961
|
-
type: "
|
|
56962
|
-
category: "
|
|
57361
|
+
type: "dynamic_method_call",
|
|
57362
|
+
category: "Route confusion (dynamic method dispatch)",
|
|
56963
57363
|
severity: "high",
|
|
56964
57364
|
evidence: l.trim(),
|
|
56965
|
-
detail: "
|
|
57365
|
+
detail: "Dynamic method/static call `" + m[1] + "$var()` — the invoked symbol is attacker-influenced."
|
|
56966
57366
|
});
|
|
56967
57367
|
}
|
|
56968
57368
|
}
|
|
56969
|
-
function
|
|
56970
|
-
const ls =
|
|
56971
|
-
const re = /\b(
|
|
57369
|
+
function detectDynamicInclude(code, file, out) {
|
|
57370
|
+
const ls = lines(code);
|
|
57371
|
+
const re = /\b(include|require)(_once)?\s*\(\s*(\$[A-Za-z_]\w*)/g;
|
|
56972
57372
|
let m;
|
|
56973
57373
|
while ((m = re.exec(code)) !== null) {
|
|
56974
|
-
const ln =
|
|
57374
|
+
const ln = lineNo(code, m.index);
|
|
56975
57375
|
const l = ls[ln - 1] ?? "";
|
|
56976
57376
|
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
56977
57377
|
`);
|
|
56978
|
-
if (!
|
|
57378
|
+
if (!userControlled(l) && !userControlled(scope))
|
|
56979
57379
|
continue;
|
|
56980
|
-
if (
|
|
57380
|
+
if (CANONICALIZE.test(scope + `
|
|
56981
57381
|
` + l))
|
|
56982
57382
|
continue;
|
|
56983
|
-
|
|
56984
|
-
file,
|
|
56985
|
-
line: ln,
|
|
56986
|
-
type: "xxe",
|
|
56987
|
-
category: "XXE (unprotected XML parse)",
|
|
56988
|
-
severity: "high",
|
|
56989
|
-
evidence: l.trim(),
|
|
56990
|
-
detail: "User-controlled XML parsed without disabling external entities — XXE (file read, SSRF, billion-laughs DoS)."
|
|
56991
|
-
});
|
|
56992
|
-
}
|
|
56993
|
-
}
|
|
56994
|
-
function detectSsti(code, file, out) {
|
|
56995
|
-
const ls = lines2(code);
|
|
56996
|
-
const re = /\b(render_template_string|Template\s*\(|res\.render\s*\(|ejs\.(render|compile)\s*\(|_\.template\s*\(|render_string|twig\.render|Jinja2\(|jinja2\.Template)\s*\(?/g;
|
|
56997
|
-
let m;
|
|
56998
|
-
while ((m = re.exec(code)) !== null) {
|
|
56999
|
-
const ln = lineNo2(code, m.index);
|
|
57000
|
-
const l = ls[ln - 1] ?? "";
|
|
57001
|
-
const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
|
|
57002
|
-
`);
|
|
57003
|
-
if (!SOURCE_TOKENS3.test(l) && !SOURCE_TOKENS3.test(scope))
|
|
57383
|
+
if (WHITELIST_LOOKUP.test(l) || WHITELIST_LOOKUP.test(scope))
|
|
57004
57384
|
continue;
|
|
57005
57385
|
out.push({
|
|
57006
57386
|
file,
|
|
57007
57387
|
line: ln,
|
|
57008
|
-
type: "
|
|
57009
|
-
category: "
|
|
57388
|
+
type: "dynamic_include",
|
|
57389
|
+
category: "Route confusion (dynamic file include)",
|
|
57010
57390
|
severity: "high",
|
|
57011
57391
|
evidence: l.trim(),
|
|
57012
|
-
detail:
|
|
57392
|
+
detail: `\`${m[1]}(…)\` of a variable — potential LFI/RCE when the path is attacker-controlled.`
|
|
57013
57393
|
});
|
|
57014
57394
|
}
|
|
57015
57395
|
}
|
|
57016
|
-
function
|
|
57017
|
-
|
|
57018
|
-
const ls = lines2(code);
|
|
57019
|
-
const re = /\b(move_uploaded_file|wp_upload_bits|->move\s*\(|storeAs\s*\(|->storeAs\s*\(|putFile\s*\()\s*\(/g;
|
|
57396
|
+
function detectBatchForwarding(code, file, out) {
|
|
57397
|
+
BATCH_LOOP_RE.lastIndex = 0;
|
|
57020
57398
|
let m;
|
|
57021
|
-
while ((m =
|
|
57022
|
-
const ln =
|
|
57023
|
-
const
|
|
57024
|
-
const scope = ls.slice(Math.max(0, ln - 30), ln + 3).join(`
|
|
57399
|
+
while ((m = BATCH_LOOP_RE.exec(code)) !== null) {
|
|
57400
|
+
const ln = lineNo(code, m.index);
|
|
57401
|
+
const body = lines(code).slice(ln - 1, ln + 8).join(`
|
|
57025
57402
|
`);
|
|
57026
|
-
|
|
57403
|
+
const forwards = /\b(call_user_func|forward|dispatch|resolve_route|invoke|handle|route)\s*\(/i.test(body);
|
|
57404
|
+
if (!forwards)
|
|
57027
57405
|
continue;
|
|
57028
|
-
|
|
57406
|
+
const rechecks = /\b(current_user_can|permission_callback|check_ajax_referer|authorize|authorise|is_user_logged_in)\b/i.test(body);
|
|
57407
|
+
if (rechecks)
|
|
57029
57408
|
continue;
|
|
57030
57409
|
out.push({
|
|
57031
57410
|
file,
|
|
57032
57411
|
line: ln,
|
|
57033
|
-
type: "
|
|
57034
|
-
category: "
|
|
57412
|
+
type: "batch_forwarding",
|
|
57413
|
+
category: "Route confusion (batch/proxy forwarding)",
|
|
57035
57414
|
severity: "high",
|
|
57036
|
-
evidence:
|
|
57037
|
-
detail: "
|
|
57038
|
-
});
|
|
57039
|
-
}
|
|
57040
|
-
}
|
|
57041
|
-
function detectUnrestrictedUpload(code, file, out) {
|
|
57042
|
-
const ls = lines2(code);
|
|
57043
|
-
const re = /['"]uploadAllow['"]\s*=>\s*(?:array\s*\(\s*)?['"]all['"]/g;
|
|
57044
|
-
let m;
|
|
57045
|
-
while ((m = re.exec(code)) !== null) {
|
|
57046
|
-
const ln = lineNo2(code, m.index);
|
|
57047
|
-
const l = ls[ln - 1] ?? "";
|
|
57048
|
-
const scope = ls.slice(Math.max(0, ln - 4), ln + 4).join(`
|
|
57049
|
-
`);
|
|
57050
|
-
const denyFirst = /['"]uploadOrder['"]\s*=>\s*array\s*\(\s*['"]deny['"]/.test(scope);
|
|
57051
|
-
out.push({
|
|
57052
|
-
file,
|
|
57053
|
-
line: ln,
|
|
57054
|
-
type: "file_upload",
|
|
57055
|
-
category: "Unrestricted upload config (uploadAllow=all)",
|
|
57056
|
-
severity: denyFirst ? "high" : "medium",
|
|
57057
|
-
evidence: l.trim(),
|
|
57058
|
-
detail: denyFirst ? "File-manager/connector config sets uploadAllow='all' with uploadOrder deny→allow — every mimetype (including .php) is uploadable → RCE." : "File-manager/connector config sets uploadAllow='all' — any file type can be uploaded; verify the upload path is not web-reachable or that a server-side extension whitelist exists."
|
|
57415
|
+
evidence: lines(code)[ln - 1]?.trim() ?? "",
|
|
57416
|
+
detail: "Batch/proxy loop forwards to inner handlers with no per-route authorization re-check — an attacker may reach a protected handler."
|
|
57059
57417
|
});
|
|
57060
57418
|
}
|
|
57061
57419
|
}
|
|
57062
|
-
function
|
|
57063
|
-
const ls =
|
|
57064
|
-
|
|
57065
|
-
|
|
57066
|
-
|
|
57067
|
-
|
|
57068
|
-
const
|
|
57069
|
-
const scope = ls.slice(Math.max(0, ln - 6), ln + 1).join(`
|
|
57420
|
+
function detectRouteNormalization(code, file, out) {
|
|
57421
|
+
const ls = lines(code);
|
|
57422
|
+
for (let i = 0;i < ls.length; i++) {
|
|
57423
|
+
const l = ls[i];
|
|
57424
|
+
if (!ROUTE_MATCH_RE.test(l))
|
|
57425
|
+
continue;
|
|
57426
|
+
const scope = ls.slice(Math.max(0, i - 3), i + 1).join(`
|
|
57070
57427
|
`);
|
|
57071
|
-
if (
|
|
57428
|
+
if (!userControlled(scope))
|
|
57072
57429
|
continue;
|
|
57073
|
-
if (
|
|
57430
|
+
if (NORMALIZERS.test(l) || NORMALIZERS.test(scope))
|
|
57074
57431
|
continue;
|
|
57075
|
-
|
|
57076
|
-
const assignConcat = /(?:^|\n)\s*\$[A-Za-z_]\w*\s*(?:\.=|=)\s*[^;\n]*["'][^"']*["']\s*\.\s*\$[A-Za-z_]\w*/.test(scope);
|
|
57077
|
-
if (!inlineConcat && !assignConcat)
|
|
57432
|
+
if (/preg_match\s*\(\s*["'][^"']*\^[^"']*\$[^"']*["']/.test(l))
|
|
57078
57433
|
continue;
|
|
57079
57434
|
out.push({
|
|
57080
57435
|
file,
|
|
57081
|
-
line:
|
|
57082
|
-
type: "
|
|
57083
|
-
category: "
|
|
57436
|
+
line: i + 1,
|
|
57437
|
+
type: "route_normalization_gap",
|
|
57438
|
+
category: "Route confusion (missing path normalization)",
|
|
57084
57439
|
severity: "medium",
|
|
57085
57440
|
evidence: l.trim(),
|
|
57086
|
-
detail: "
|
|
57441
|
+
detail: "Route matched against user input without visible normalization (strtolower/rtrim/urldecode) — route desync possible."
|
|
57087
57442
|
});
|
|
57088
57443
|
}
|
|
57089
57444
|
}
|
|
57090
|
-
|
|
57091
|
-
|
|
57092
|
-
|
|
57093
|
-
|
|
57094
|
-
|
|
57095
|
-
|
|
57096
|
-
|
|
57097
|
-
|
|
57098
|
-
let m;
|
|
57099
|
-
while ((m = SECRET_FIELD_RE.exec(clean)) !== null) {
|
|
57100
|
-
const field = m[1];
|
|
57101
|
-
const rest = clean.slice(m.index + m[0].length);
|
|
57102
|
-
const lineEnd = rest.search(/\r?\n/);
|
|
57103
|
-
const valueRaw = (lineEnd === -1 ? rest : rest.slice(0, lineEnd)).trim();
|
|
57104
|
-
let value = "";
|
|
57105
|
-
const qm = valueRaw.match(/^["']([^"']{4,})["']/);
|
|
57106
|
-
if (qm)
|
|
57107
|
-
value = qm[1];
|
|
57108
|
-
else
|
|
57109
|
-
value = (valueRaw.match(/^([A-Za-z0-9._/+=@-]{8,})/) ?? [])[1] ?? "";
|
|
57110
|
-
if (!value)
|
|
57111
|
-
continue;
|
|
57112
|
-
if (SECRET_PLACEHOLDER_RE.test(value))
|
|
57113
|
-
continue;
|
|
57114
|
-
if (/\$[A-Za-z_]|[{}\[\]]|process\.env|getenv|os\.environ|config\s*\(|env\s*\(/.test(valueRaw))
|
|
57115
|
-
continue;
|
|
57116
|
-
const ln = lineNo2(code, m.index);
|
|
57117
|
-
flaggedLines.add(ln);
|
|
57118
|
-
out.push({
|
|
57119
|
-
file,
|
|
57120
|
-
line: ln,
|
|
57121
|
-
type: "hardcoded_secret",
|
|
57122
|
-
category: "Hardcoded secret (credential in source)",
|
|
57123
|
-
severity: "high",
|
|
57124
|
-
evidence: (ls[ln - 1] ?? "").trim(),
|
|
57125
|
-
detail: `Secret field '${field}' is assigned a hardcoded value in source — the credential is exposed to anyone with code access (CWE-798 Hard-coded Credentials). Rotate it and load from the environment instead.`
|
|
57126
|
-
});
|
|
57127
|
-
}
|
|
57128
|
-
for (const fmt of SECRET_FORMATS) {
|
|
57129
|
-
fmt.re.lastIndex = 0;
|
|
57130
|
-
let fm;
|
|
57131
|
-
while ((fm = fmt.re.exec(clean)) !== null) {
|
|
57132
|
-
const ln = lineNo2(code, fm.index);
|
|
57133
|
-
if (flaggedLines.has(ln))
|
|
57134
|
-
continue;
|
|
57135
|
-
flaggedLines.add(ln);
|
|
57136
|
-
out.push({
|
|
57137
|
-
file,
|
|
57138
|
-
line: ln,
|
|
57139
|
-
type: "hardcoded_secret",
|
|
57140
|
-
category: "Hardcoded secret (known format)",
|
|
57141
|
-
severity: "high",
|
|
57142
|
-
evidence: (ls[ln - 1] ?? "").trim(),
|
|
57143
|
-
detail: `A ${fmt.name} was found hardcoded in source — the credential is exposed to anyone with code access (CWE-798). Rotate it and load from the environment instead.`
|
|
57144
|
-
});
|
|
57145
|
-
}
|
|
57146
|
-
}
|
|
57147
|
-
const isMinified = /\.min\.(js|css)$/i.test(file) || /[\\/](bundle|vendor|dist|build)[\\/]/i.test(file);
|
|
57148
|
-
if (!isMinified) {
|
|
57149
|
-
const entropyRe = /(\b[A-Za-z_]\w*)\s*(?:::|=>|=|:)\s*["']([A-Za-z0-9_-]{32,})["']/g;
|
|
57150
|
-
entropyRe.lastIndex = 0;
|
|
57151
|
-
let em;
|
|
57152
|
-
while ((em = entropyRe.exec(clean)) !== null) {
|
|
57153
|
-
const varName = em[1];
|
|
57154
|
-
const value = em[2];
|
|
57155
|
-
const ln = lineNo2(code, em.index);
|
|
57156
|
-
if (flaggedLines.has(ln))
|
|
57157
|
-
continue;
|
|
57158
|
-
if (ENTROPY_EXCLUDE_VAR.test(varName))
|
|
57159
|
-
continue;
|
|
57160
|
-
if (!/[A-Z]/.test(value) || !/[a-z]/.test(value) || !/[0-9]/.test(value))
|
|
57161
|
-
continue;
|
|
57162
|
-
if (shannonEntropy(value) < 4.5)
|
|
57163
|
-
continue;
|
|
57164
|
-
flaggedLines.add(ln);
|
|
57165
|
-
out.push({
|
|
57166
|
-
file,
|
|
57167
|
-
line: ln,
|
|
57168
|
-
type: "hardcoded_secret",
|
|
57169
|
-
category: "Suspected secret (high-entropy token)",
|
|
57170
|
-
severity: "medium",
|
|
57171
|
-
evidence: (ls[ln - 1] ?? "").trim(),
|
|
57172
|
-
detail: `'${varName}' is assigned a high-entropy token (Shannon ${shannonEntropy(value).toFixed(2)} bits/char) that looks like a generated credential — verify it is not a secret key/token (CWE-798).`
|
|
57173
|
-
});
|
|
57174
|
-
}
|
|
57175
|
-
}
|
|
57445
|
+
function detectRouteConfusion(code, file) {
|
|
57446
|
+
const out = [];
|
|
57447
|
+
detectDynamicDispatch(code, file, out);
|
|
57448
|
+
detectDynamicMethod(code, file, out);
|
|
57449
|
+
detectDynamicInclude(code, file, out);
|
|
57450
|
+
detectBatchForwarding(code, file, out);
|
|
57451
|
+
detectRouteNormalization(code, file, out);
|
|
57452
|
+
return out;
|
|
57176
57453
|
}
|
|
57177
57454
|
|
|
57455
|
+
// src/server.ts
|
|
57456
|
+
init_complex_bugs();
|
|
57457
|
+
|
|
57178
57458
|
// src/guidance.ts
|
|
57179
57459
|
var SINK_DIRECTIVES = {
|
|
57180
57460
|
sql_execution: "Validate the SQL injection live with strike_verify (use a unique MARKER and a NEGATIVE CONTROL that stays inert), or read_tool_manual for sqlmap. Only a reflected marker + inert control = confirmed.",
|
|
@@ -57393,11 +57673,15 @@ function dataRefresh() {
|
|
|
57393
57673
|
};
|
|
57394
57674
|
}
|
|
57395
57675
|
|
|
57676
|
+
// src/server.ts
|
|
57677
|
+
init_verify_harness();
|
|
57678
|
+
|
|
57396
57679
|
// src/benchmark.ts
|
|
57397
57680
|
import { readFileSync as readFileSync19, readdirSync as readdirSync5, writeFileSync as writeFileSync6, rmSync as rmSync3, mkdirSync as mkdirSync7 } from "node:fs";
|
|
57398
57681
|
import { join as join18 } from "node:path";
|
|
57399
57682
|
import { fileURLToPath as fileURLToPath10 } from "node:url";
|
|
57400
57683
|
import { tmpdir as tmpdir3, homedir as homedir8 } from "node:os";
|
|
57684
|
+
init_complex_bugs();
|
|
57401
57685
|
var ROOT5 = join18(fileURLToPath10(new URL(".", import.meta.url)), "..");
|
|
57402
57686
|
var CORPUS = join18(ROOT5, "bench", "corpus");
|
|
57403
57687
|
var CAPTURED_CORPUS_DIR = process.env.BLITZSTRIKE_CORPUS_DIR ?? join18(homedir8(), ".blitzstrike", "corpus");
|
|
@@ -57693,8 +57977,13 @@ function openCount() {
|
|
|
57693
57977
|
return loadObligations().filter((o) => o.status === "open").length;
|
|
57694
57978
|
}
|
|
57695
57979
|
|
|
57980
|
+
// src/server.ts
|
|
57981
|
+
init_security_state();
|
|
57982
|
+
init_framework_security();
|
|
57983
|
+
|
|
57696
57984
|
// src/variant-scan.ts
|
|
57697
57985
|
init_scanner();
|
|
57986
|
+
init_complex_bugs();
|
|
57698
57987
|
import { readFileSync as readFileSync22 } from "node:fs";
|
|
57699
57988
|
|
|
57700
57989
|
// src/cross-file.ts
|
|
@@ -58592,6 +58881,7 @@ function analyzeJwt(token, opts = {}) {
|
|
|
58592
58881
|
// src/enterprise-benchmark.ts
|
|
58593
58882
|
import { existsSync as existsSync11, readFileSync as readFileSync23, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
|
|
58594
58883
|
import { join as join20, resolve } from "node:path";
|
|
58884
|
+
init_complex_bugs();
|
|
58595
58885
|
var ROOT6 = resolve(process.cwd(), "bench", "enterprise");
|
|
58596
58886
|
var ENTERPRISE_FIXTURES = [
|
|
58597
58887
|
{ id: "php-laravel", dir: join20(ROOT6, "php-laravel"), expected_vulns: 4 },
|
|
@@ -59245,6 +59535,13 @@ ${statusLine}` : statusLine },
|
|
|
59245
59535
|
}, async () => {
|
|
59246
59536
|
return { content: [{ type: "text", text: JSON.stringify(dataRefresh()) }] };
|
|
59247
59537
|
});
|
|
59538
|
+
server.registerTool("run_verification", {
|
|
59539
|
+
title: "Detector verification harness",
|
|
59540
|
+
description: "VERIFY: run every detector against its positive (must fire) + negative (must stay silent) fixture and report TP/FP/FN per detector. This is the reproducible 'battle-tested' proof — run it to confirm the detectors still pass after any change.",
|
|
59541
|
+
inputSchema: {}
|
|
59542
|
+
}, async () => {
|
|
59543
|
+
return { content: [{ type: "text", text: JSON.stringify(runVerificationHarness()) }] };
|
|
59544
|
+
});
|
|
59248
59545
|
server.registerTool("fofa_search", {
|
|
59249
59546
|
title: "FOFA asset search",
|
|
59250
59547
|
description: "STRIKE: search the FOFA asset index (needs FOFA_EMAIL + FOFA_KEY).",
|
|
@@ -60613,6 +60910,7 @@ Usage:
|
|
|
60613
60910
|
blitzstrike install-tools --category recon --dry-run Preview / filter
|
|
60614
60911
|
blitzstrike sync-data Fetch heavy datasets (payloads + templates) on-demand
|
|
60615
60912
|
blitzstrike update Check for a newer version + refresh the data cache
|
|
60913
|
+
blitzstrike verify Run every detector against TP/FP fixtures + report
|
|
60616
60914
|
blitzstrike version Print version
|
|
60617
60915
|
blitzstrike --help This help`;
|
|
60618
60916
|
}
|
|
@@ -60664,6 +60962,14 @@ async function main() {
|
|
|
60664
60962
|
console.log(` data: payloads ${r.data_sync.payloads.files} files, templates ${r.data_sync.templates.files} files ${r.data_sync.payloads.synced ? "(refreshed)" : ""}`);
|
|
60665
60963
|
return;
|
|
60666
60964
|
}
|
|
60965
|
+
if (args[0] === "verify") {
|
|
60966
|
+
await Promise.resolve().then(() => init_verify_harness());
|
|
60967
|
+
console.log(verificationReportText());
|
|
60968
|
+
const r = runVerificationHarness();
|
|
60969
|
+
if (r.failed > 0)
|
|
60970
|
+
process.exit(1);
|
|
60971
|
+
return;
|
|
60972
|
+
}
|
|
60667
60973
|
if (args[0] === "serve") {
|
|
60668
60974
|
await serve();
|
|
60669
60975
|
return;
|