redgun-security 1.2.0 → 1.4.0
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/README.md +1 -1
- package/package.json +1 -1
- package/scan.js +18 -0
- package/src/local/client-proto.js +67 -0
- package/src/local/css-injection.js +67 -0
- package/src/local/csti.js +71 -0
- package/src/local/electron.js +68 -0
- package/src/local/index.js +26 -0
- package/src/local/jwt-advanced.js +70 -0
- package/src/local/llm-ai.js +67 -0
- package/src/local/padding-oracle.js +66 -0
- package/src/local/postmessage.js +66 -0
- package/src/local/service-worker.js +64 -0
- package/src/local/supply-chain-advanced.js +71 -0
- package/src/local/timing.js +66 -0
- package/src/local/webauthn.js +66 -0
- package/src/local/xpath-ssi.js +67 -0
- package/src/remote/complete.js +240 -0
- package/src/remote/modern.js +181 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
import { addFinding } from '../core/findings.js';
|
|
4
|
+
|
|
5
|
+
const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.html', '.htm', '.vue', '.svelte', '.astro'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
|
|
7
|
+
|
|
8
|
+
export async function auditPostMessage(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for PostMessage/BroadcastChannel vulnerabilities...';
|
|
10
|
+
const files = getFiles(projectPath);
|
|
11
|
+
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(file, 'utf-8');
|
|
15
|
+
const relativePath = file.replace(projectPath, '.');
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
|
|
18
|
+
const patterns = [
|
|
19
|
+
{ pattern: /addEventListener\s*\(\s*['"]message['"]\s*(?!.*\.origin)/gi, name: 'message listener without origin check', severity: 'CRITICAL' },
|
|
20
|
+
{ pattern: /addEventListener\s*\(\s*['"]message['"]\s*,\s*\([^)]*\)\s*=>\s*\{(?!.*origin)/gi, name: 'postMessage handler arrow fn without origin validation', severity: 'CRITICAL' },
|
|
21
|
+
{ pattern: /postMessage\s*\(\s*[^,]*\s*,\s*['"]\*['"]/gi, name: 'postMessage with wildcard targetOrigin *', severity: 'HIGH' },
|
|
22
|
+
{ pattern: /event\.origin\s*\.includes|event\.origin\s*\.endsWith|event\.origin\s*\.startsWith/gi, name: 'origin check using includes/endsWith (substring bypass)', severity: 'HIGH' },
|
|
23
|
+
{ pattern: /event\.origin\s*===\s*['"]https:\/\/[^'"]*['"]/gi, name: 'Strict origin comparison (good)', severity: 'INFO' },
|
|
24
|
+
{ pattern: /eval\s*\(\s*event\.data|innerHTML\s*=\s*event\.data|document\.write\s*\(\s*event\.data/gi, name: 'postMessage data used dangerously', severity: 'CRITICAL' },
|
|
25
|
+
{ pattern: /new\s+BroadcastChannel\s*\(/gi, name: 'BroadcastChannel created (broadcast to all same-origin tabs)', severity: 'MEDIUM' },
|
|
26
|
+
{ pattern: /new\s+MessageChannel\s*\(/gi, name: 'MessageChannel created (check port1/port2 exposure)', severity: 'LOW' },
|
|
27
|
+
{ pattern: /window\.opener\.postMessage|parent\.postMessage|top\.postMessage/gi, name: 'Cross-window postMessage (check origin)', severity: 'MEDIUM' },
|
|
28
|
+
{ pattern: /iframe.*contentWindow\.postMessage|iframe.*\.src/gi, name: 'Iframe postMessage interaction', severity: 'MEDIUM' },
|
|
29
|
+
{ pattern: /window\.open\s*\(\s*['"`].*['"`]\s*\)/gi, name: 'window.open (tabnabbing if no opener policy)', severity: 'LOW' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
for (const { pattern, name, severity } of patterns) {
|
|
33
|
+
let match;
|
|
34
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
35
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
36
|
+
const line = lines[lineNum - 1]?.trim() || '';
|
|
37
|
+
if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*')) continue;
|
|
38
|
+
|
|
39
|
+
addFinding(
|
|
40
|
+
severity,
|
|
41
|
+
'PostMessage / BroadcastChannel',
|
|
42
|
+
name,
|
|
43
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
44
|
+
'Always validate event.origin with strict equality. Use a whitelist, not includes/endsWith. Never evaluate event.data as code. Use BroadcastChannel with user validation. For window.open, set opener=null and use noopener/noreferrer.'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getFiles(dir, files = []) {
|
|
53
|
+
try {
|
|
54
|
+
const entries = readdirSync(dir);
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
|
|
57
|
+
const fullPath = join(dir, entry);
|
|
58
|
+
try {
|
|
59
|
+
const stat = statSync(fullPath);
|
|
60
|
+
if (stat.isDirectory()) getFiles(fullPath, files);
|
|
61
|
+
else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
} catch {}
|
|
65
|
+
return files;
|
|
66
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
import { addFinding } from '../core/findings.js';
|
|
4
|
+
|
|
5
|
+
const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.html', '.htm'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
|
|
7
|
+
|
|
8
|
+
export async function auditServiceWorker(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for Service Worker abuse vectors...';
|
|
10
|
+
const files = getFiles(projectPath);
|
|
11
|
+
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(file, 'utf-8');
|
|
15
|
+
const relativePath = file.replace(projectPath, '.');
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
|
|
18
|
+
const patterns = [
|
|
19
|
+
{ pattern: /navigator\.serviceWorker\.register\s*\(/gi, name: 'Service Worker registration', severity: 'INFO' },
|
|
20
|
+
{ pattern: /importScripts\s*\(/gi, name: 'Service Worker importScripts (XSS via sw)', severity: 'HIGH' },
|
|
21
|
+
{ pattern: /self\.addEventListener\s*\(\s*['"]fetch['"]/gi, name: 'Service Worker fetch listener', severity: 'INFO' },
|
|
22
|
+
{ pattern: /self\.addEventListener\s*\(\s*['"]message['"]/gi, name: 'Service Worker message listener (postMessage attack)', severity: 'MEDIUM' },
|
|
23
|
+
{ pattern: /respondWith\s*\(\s*fetch\s*\(\s*event\.request\s*\)/gi, name: 'SW passthrough fetch (check URL rewriting)', severity: 'MEDIUM' },
|
|
24
|
+
{ pattern: /event\.respondWith\s*\(\s*new\s+Response/gi, name: 'SW custom response (check for content injection)', severity: 'MEDIUM' },
|
|
25
|
+
{ pattern: /CacheStorage|self\.caches\s*\./gi, name: 'Cache API usage (cache poisoning via SW)', severity: 'LOW' },
|
|
26
|
+
{ pattern: /Clients\.claim\s*\(\s*\)|self\.clients\.claim/gi, name: 'Service Worker immediate claim (malicious takeover)', severity: 'HIGH' },
|
|
27
|
+
{ pattern: /self\.skipWaiting/gi, name: 'SW skipWaiting (immediate activation)', severity: 'MEDIUM' },
|
|
28
|
+
{ pattern: /navigator\.serviceWorker\.getRegistration|navigator\.serviceWorker\.controller/gi, name: 'SW control check', severity: 'INFO' },
|
|
29
|
+
{ pattern: /self\.registration\.unregister/gi, name: 'SW unregistration', severity: 'LOW' },
|
|
30
|
+
{ pattern: /PushManager|pushManager\.subscribe|showNotification/gi, name: 'Push notifications (notification spam vector)', severity: 'LOW' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
for (const { pattern, name, severity } of patterns) {
|
|
34
|
+
let match;
|
|
35
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
36
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
37
|
+
addFinding(
|
|
38
|
+
severity,
|
|
39
|
+
'Service Worker / WebRTC',
|
|
40
|
+
name,
|
|
41
|
+
`File: ${relativePath}:${lineNum}\nCode: ${lines[lineNum - 1]?.trim().substring(0, 120)}`,
|
|
42
|
+
'Restrict Service Worker scope (/). Use importScripts from same-origin only. Validate fetch events to prevent response tampering. Monitor dangerously overwritable headers. Limit Cache API usage to prevent storage exhaustion.'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getFiles(dir, files = []) {
|
|
51
|
+
try {
|
|
52
|
+
const entries = readdirSync(dir);
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
|
|
55
|
+
const fullPath = join(dir, entry);
|
|
56
|
+
try {
|
|
57
|
+
const stat = statSync(fullPath);
|
|
58
|
+
if (stat.isDirectory()) getFiles(fullPath, files);
|
|
59
|
+
else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
} catch {}
|
|
63
|
+
return files;
|
|
64
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
import { addFinding } from '../core/findings.js';
|
|
4
|
+
|
|
5
|
+
const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.json', '.lock', '.toml'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
|
|
7
|
+
|
|
8
|
+
export async function auditSupplyChainAdvanced(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for supply chain attacks (dep confusion, lockfile, post-install)...';
|
|
10
|
+
const files = getFiles(projectPath);
|
|
11
|
+
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(file, 'utf-8');
|
|
15
|
+
const relativePath = file.replace(projectPath, '.');
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
|
|
18
|
+
const patterns = [
|
|
19
|
+
{ pattern: /postinstall|preinstall|postuninstall|prepare|prepublishOnly/gi, name: 'npm lifecycle hook (check for abuse)', severity: 'MEDIUM' },
|
|
20
|
+
{ pattern: /"postinstall"\s*:\s*"([^"]+)"/gi, name: 'postinstall script detected', severity: 'HIGH' },
|
|
21
|
+
{ pattern: /"scripts"\s*:\s*\{[^}]*"(?:install|postinstall|preinstall)"/gi, name: 'Install script in package.json', severity: 'MEDIUM' },
|
|
22
|
+
{ pattern: /(?:eval|exec|child_process|spawn|require)\s*\(\s*['"`][^'"`]*(['"`]\s*\+\s*|['"`]\s*\+\s*['"`])/gi, name: 'Dynamic code execution in scripts', severity: 'CRITICAL' },
|
|
23
|
+
{ pattern: /npm_config_|process\.env\.npm_/gi, name: 'npm config env var usage (can be manipulated)', severity: 'MEDIUM' },
|
|
24
|
+
{ pattern: /(?:registry|publishConfig|_resolved)\s*:\s*"(?!https:\/\/registry\.npmjs\.org)/gi, name: 'Non-standard npm registry configured', severity: 'HIGH' },
|
|
25
|
+
{ pattern: /"dependencies"\s*:\s*\{[^}]*"(?!@[^"]+)":/gi, name: 'Unscoped dependency (dep confusion risk)', severity: 'MEDIUM' },
|
|
26
|
+
{ pattern: /"resolved"\s*:\s*"[^"]*(?:localhost|0\.0\.0\.0|127\.0\.0\.1|evil|hack|malware)/gi, name: 'Suspicious resolved URL in lockfile', severity: 'CRITICAL' },
|
|
27
|
+
{ pattern: /"version"\s*:\s*"0\.0\.\d+|"version"\s*:\s*"file:|\*"resolved"\s*"/gi, name: 'Zero-version or file: dependency (suspicious)', severity: 'HIGH' },
|
|
28
|
+
{ pattern: /gist\.githubusercontent\.com|raw\.githubusercontent\.com.*npm|npm\s*-?\s*i\s*-\s*g\s*http/i, name: 'Package from non-registry source', severity: 'HIGH' },
|
|
29
|
+
{ pattern: /(?:integrity|shasum|sha512|sha1)\s*:\s*""/gi, name: 'Empty integrity hash in lockfile (tampered)', severity: 'CRITICAL' },
|
|
30
|
+
{ pattern: /"hasInstallScript"\s*:\s*true/gi, name: 'Package has install scripts (audit)', severity: 'MEDIUM' },
|
|
31
|
+
{ pattern: /npmrc|\.npmrc.*token|npm.*config.*set.*auth/gi, name: 'npm registry auth config', severity: 'INFO' },
|
|
32
|
+
{ pattern: /(?:pip|pip3|pipx)\s+install\s+[-]+\s*(?!-r)/gi, name: 'pip install without requirements (dep confusion)', severity: 'MEDIUM' },
|
|
33
|
+
{ pattern: /curl\s+.*\|\s*(?:bash|sh|zsh)/gi, name: 'curl pipe to shell (remote code execution risk)', severity: 'CRITICAL' },
|
|
34
|
+
{ pattern: /wget\s+.*\|\s*(?:bash|sh|zsh)/gi, name: 'wget pipe to shell (remote code execution risk)', severity: 'CRITICAL' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
for (const { pattern, name, severity } of patterns) {
|
|
38
|
+
let match;
|
|
39
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
40
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
41
|
+
const line = lines[lineNum - 1]?.trim() || '';
|
|
42
|
+
if (line.startsWith('#') || line.startsWith('//')) continue;
|
|
43
|
+
|
|
44
|
+
addFinding(
|
|
45
|
+
severity,
|
|
46
|
+
'Supply Chain Attacks',
|
|
47
|
+
name,
|
|
48
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
49
|
+
'Use scoped packages (@org/pkg) to prevent dependency confusion. Audit postinstall scripts. Verify lockfile integrity. Use npm audit and npm vet. Avoid curl|bash patterns. Pin dependencies with exact versions and verify hashes. Use private registry for internal packages.'
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getFiles(dir, files = []) {
|
|
58
|
+
try {
|
|
59
|
+
const entries = readdirSync(dir);
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
|
|
62
|
+
const fullPath = join(dir, entry);
|
|
63
|
+
try {
|
|
64
|
+
const stat = statSync(fullPath);
|
|
65
|
+
if (stat.isDirectory()) getFiles(fullPath, files);
|
|
66
|
+
else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
} catch {}
|
|
70
|
+
return files;
|
|
71
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
import { addFinding } from '../core/findings.js';
|
|
4
|
+
|
|
5
|
+
const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.py', '.rb', '.php', '.go', '.java'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
|
|
7
|
+
|
|
8
|
+
export async function auditTiming(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for timing side-channel vulnerabilities...';
|
|
10
|
+
const files = getFiles(projectPath);
|
|
11
|
+
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(file, 'utf-8');
|
|
15
|
+
const relativePath = file.replace(projectPath, '.');
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
|
|
18
|
+
const patterns = [
|
|
19
|
+
{ pattern: /(?:password|secret|token|key|hash|pin).*\s*={2,3}\s*(?:req|request|params|body|input)/gi, name: 'String comparison with user input (timing leak)', severity: 'HIGH' },
|
|
20
|
+
{ pattern: /(?:password|secret|token|key|hash|pin)\s*===\s*/gi, name: 'Strict comparison used (good for timing)', severity: 'INFO' },
|
|
21
|
+
{ pattern: /crypto\.timingSafeEqual|timingsafe\b/gi, name: 'Timing-safe comparison used (good)', severity: 'INFO' },
|
|
22
|
+
{ pattern: /\b(?:sleep|delay|wait|setTimeout|setInterval)\s*\(\s*(?:req|request|params|query|body)/gi, name: 'User-controlled delay/sleep (potential timing oracle)', severity: 'MEDIUM' },
|
|
23
|
+
{ pattern: /(?:if|when)\s*\([^)]*(?:req|request|params|query|body)[^)]*\)\s*(?:\{|\n)\s*(?:sleep|delay|setTimeout)/gi, name: 'Conditional sleep based on user input', severity: 'HIGH' },
|
|
24
|
+
{ pattern: /(?:login|auth|signin).*select.*password.*(?:req|request|params)/gi, name: 'Password DB lookup (check for timing leak on user-not-found)', severity: 'MEDIUM' },
|
|
25
|
+
{ pattern: /bcrypt\.compare|bcrypt\.compareSync|argon2\.verify/gi, name: 'bcrypt/argon2 verification (timing-safe)', severity: 'INFO' },
|
|
26
|
+
{ pattern: /(?:hash|digest|hmac)\s*\(\s*['"]?(?:md5|sha1|sha256|sha512)['"]?\s*,/gi, name: 'Hash function usage (check HMAC timing)', severity: 'LOW' },
|
|
27
|
+
{ pattern: /(?:for|while)\s*\([^)]*(?:req|request|params|query|body)[^)]*\)/gi, name: 'Loop iteration from user input (DoS + timing oracle)', severity: 'MEDIUM' },
|
|
28
|
+
{ pattern: /(?:username|email|user).*exists|(?:findOne|findUser)\s*\(\s*\{.*username/gi, name: 'User existence check (potential oracle)', severity: 'LOW' },
|
|
29
|
+
{ pattern: /PasswordResetListener|SentMessage|mail.*send.*token/gi, name: 'Email sending after reset request (timing oracle on user existence)', severity: 'LOW' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
for (const { pattern, name, severity } of patterns) {
|
|
33
|
+
let match;
|
|
34
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
35
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
36
|
+
const line = lines[lineNum - 1]?.trim() || '';
|
|
37
|
+
if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*')) continue;
|
|
38
|
+
|
|
39
|
+
addFinding(
|
|
40
|
+
severity,
|
|
41
|
+
'Timing Side-Channels',
|
|
42
|
+
name,
|
|
43
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
44
|
+
'Use constant-time comparison (timingSafeEqual) for secrets. Ensure login returns identical responses regardless of user existence. Add random jitter to email sending. Never use sleep/delay from user input.'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getFiles(dir, files = []) {
|
|
53
|
+
try {
|
|
54
|
+
const entries = readdirSync(dir);
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
|
|
57
|
+
const fullPath = join(dir, entry);
|
|
58
|
+
try {
|
|
59
|
+
const stat = statSync(fullPath);
|
|
60
|
+
if (stat.isDirectory()) getFiles(fullPath, files);
|
|
61
|
+
else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
} catch {}
|
|
65
|
+
return files;
|
|
66
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
import { addFinding } from '../core/findings.js';
|
|
4
|
+
|
|
5
|
+
const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.py', '.rb', '.php', '.go', '.java'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
|
|
7
|
+
|
|
8
|
+
export async function auditWebauthn(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for WebAuthn/Passkeys vulnerabilities...';
|
|
10
|
+
const files = getFiles(projectPath);
|
|
11
|
+
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(file, 'utf-8');
|
|
15
|
+
const relativePath = file.replace(projectPath, '.');
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
|
|
18
|
+
const patterns = [
|
|
19
|
+
{ pattern: /navigator\.credentials\.(?:create|get)\s*\(/gi, name: 'WebAuthn credential create/get', severity: 'INFO' },
|
|
20
|
+
{ pattern: /PublicKeyCredential|CredentialCreationOptions/gi, name: 'WebAuthn API usage', severity: 'INFO' },
|
|
21
|
+
{ pattern: /(?:webauthn|passkey|security.?key|biometric).*(?:auth|login|verify|register)/gi, name: 'Passkey/WebAuthn auth flow', severity: 'INFO' },
|
|
22
|
+
{ pattern: /(?:webauthn|passkey).*.*(?:relay|proxy|forward|redirect)/gi, name: 'WebAuthn relay attack surface', severity: 'HIGH' },
|
|
23
|
+
{ pattern: /challenge\s*[:=]\s*['"][a-zA-Z0-9]{1,16}['"]|challenge\s*[:=]\s*Bufffer\.from\s*\(/gi, name: 'Short/static WebAuthn challenge (relay vector)', severity: 'HIGH' },
|
|
24
|
+
{ pattern: /(?:challenge|nonce|serverChallenge).*(?:Math\.random|Date\.now|uuid)/gi, name: 'WebAuthn challenge from predictable source', severity: 'CRITICAL' },
|
|
25
|
+
{ pattern: /allowCredentials\s*:\s*\[\s*\]|allowCredentials\s*:\s*\[\s*\{\s*type\s*:\s*'public-key'\s*\}\s*\]/gi, name: 'WebAuthn empty allowCredentials (allowed all keys)', severity: 'HIGH' },
|
|
26
|
+
{ pattern: /userVerification\s*:\s*['"]discouraged['"]/gi, name: 'WebAuthn user verification discouraged', severity: 'MEDIUM' },
|
|
27
|
+
{ pattern: /attestation\s*:\s*['"]none['"]/gi, name: 'WebAuthn attestation disabled (no device verification)', severity: 'MEDIUM' },
|
|
28
|
+
{ pattern: /rp\.id\s*[:=]\s*['"][^'"]*['"]/gi, name: 'WebAuthn relying party ID', severity: 'INFO' },
|
|
29
|
+
{ pattern: /(?:webauthn|fido2|u2f).*(?:fallback|recovery|backup).*(?:password|sms|email)/gi, name: 'WebAuthn with weak fallback (bypass vector)', severity: 'HIGH' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
for (const { pattern, name, severity } of patterns) {
|
|
33
|
+
let match;
|
|
34
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
35
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
36
|
+
const line = lines[lineNum - 1]?.trim() || '';
|
|
37
|
+
if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*')) continue;
|
|
38
|
+
|
|
39
|
+
addFinding(
|
|
40
|
+
severity,
|
|
41
|
+
'WebAuthn / Passkeys',
|
|
42
|
+
name,
|
|
43
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
44
|
+
'Use cryptographically random challenges (32+ bytes). Do not use predictable sources for challenges. Require userVerification for sensitive operations. Implement proper RP ID validation. Do not rely on WebAuthn alone; use as MFA factor.'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getFiles(dir, files = []) {
|
|
53
|
+
try {
|
|
54
|
+
const entries = readdirSync(dir);
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
|
|
57
|
+
const fullPath = join(dir, entry);
|
|
58
|
+
try {
|
|
59
|
+
const stat = statSync(fullPath);
|
|
60
|
+
if (stat.isDirectory()) getFiles(fullPath, files);
|
|
61
|
+
else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
} catch {}
|
|
65
|
+
return files;
|
|
66
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
import { addFinding } from '../core/findings.js';
|
|
4
|
+
|
|
5
|
+
const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.py', '.rb', '.php', '.go', '.java'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
|
|
7
|
+
|
|
8
|
+
export async function auditXpathSsi(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for XPath & SSI injection...';
|
|
10
|
+
const files = getFiles(projectPath);
|
|
11
|
+
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(file, 'utf-8');
|
|
15
|
+
const relativePath = file.replace(projectPath, '.');
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
|
|
18
|
+
const patterns = [
|
|
19
|
+
{ pattern: /XPath\.evaluate\s*\(\s*(?:req|request|params|query|body|user)/gi, name: 'XPath evaluate with user input', severity: 'CRITICAL' },
|
|
20
|
+
{ pattern: /xpath\.select\s*\(\s*(?:req|request|params|query|body)/gi, name: 'XPath select with user input', severity: 'CRITICAL' },
|
|
21
|
+
{ pattern: /(?:find|query|select)\s*\(\s*['"`]\/\/(?:\w+::)?(\w+)\[.*\$\{/gi, name: 'XPath query with template literal', severity: 'CRITICAL' },
|
|
22
|
+
{ pattern: /(?:find|query|select)\s*\(\s*['"`]\/\/(?:\w+::)?(\w+)\[.*\+/gi, name: 'XPath query with concatenated input', severity: 'CRITICAL' },
|
|
23
|
+
{ pattern: /SimpleXMLElement.*xpath\s*\(\s*\$_(?:GET|POST|REQUEST)/gi, name: 'PHP SimpleXML xpath user input', severity: 'CRITICAL' },
|
|
24
|
+
{ pattern: /DOMXPath.*query\s*\(\s*\$_(?:GET|POST|REQUEST)/gi, name: 'PHP DOMXPath query user input', severity: 'CRITICAL' },
|
|
25
|
+
{ pattern: /xpath\.compile\s*\(\s*(?:req|request|input)/gi, name: 'XPath compile with user input', severity: 'HIGH' },
|
|
26
|
+
{ pattern: /<!--#\s*(?:include|exec|echo|fsize|flastmod|config)/gi, name: 'Server-Side Includes (SSI) in templates', severity: 'HIGH' },
|
|
27
|
+
{ pattern: /<!--#echo\s+var|<!--#include\s+(?:virtual|file)|<!--#exec/gi, name: 'SSI directives in source', severity: 'CRITICAL' },
|
|
28
|
+
{ pattern: /(?:shtml|stm|shtm)/gi, name: 'SSI file extension (.shtml)', severity: 'MEDIUM' },
|
|
29
|
+
{ pattern: /Options\s+\+Includes|AddHandler.*server-parsed/gi, name: 'Apache SSI enabled', severity: 'LOW' },
|
|
30
|
+
{ pattern: /(?:include|exec|echo)\s*\(\s*(?:req|request|params|input)/gi, name: 'Server include with user input', severity: 'CRITICAL' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
for (const { pattern, name, severity } of patterns) {
|
|
34
|
+
let match;
|
|
35
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
36
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
37
|
+
const line = lines[lineNum - 1]?.trim() || '';
|
|
38
|
+
if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*')) continue;
|
|
39
|
+
|
|
40
|
+
addFinding(
|
|
41
|
+
severity,
|
|
42
|
+
'XPath / SSI Injection',
|
|
43
|
+
name,
|
|
44
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
45
|
+
'Use parameterized XPath queries. Sanitize input against special chars (, ), [, ], :, *, /, @, !, =, >, <. Disable SSI unless needed (Options -Includes). Never pass user input to SSI directives.'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getFiles(dir, files = []) {
|
|
54
|
+
try {
|
|
55
|
+
const entries = readdirSync(dir);
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
|
|
58
|
+
const fullPath = join(dir, entry);
|
|
59
|
+
try {
|
|
60
|
+
const stat = statSync(fullPath);
|
|
61
|
+
if (stat.isDirectory()) getFiles(fullPath, files);
|
|
62
|
+
else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
return files;
|
|
67
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { addFinding } from '../core/findings.js';
|
|
2
|
+
import { fetchText, fetchWithTimeout } from '../utils/fetch.js';
|
|
3
|
+
|
|
4
|
+
export async function scanSsrfBypassChains(origin, spinner) {
|
|
5
|
+
spinner.text = 'Testing SSRF bypass chains (DNS rebinding, redirect, encoding)...';
|
|
6
|
+
|
|
7
|
+
const ssrfParams = ['url', 'link', 'src', 'callback', 'webhook', 'fetch', 'proxy', 'redirect', 'image', 'file', 'path', 'feed'];
|
|
8
|
+
const bypassPayloads = [
|
|
9
|
+
{ name: 'IPv4 decimal', value: 'http://2130706433/' },
|
|
10
|
+
{ name: 'IPv4 hex', value: 'http://0x7f000001/' },
|
|
11
|
+
{ name: 'IPv4 octal', value: 'http://017700000001/' },
|
|
12
|
+
{ name: 'IPv4 short', value: 'http://127.1/' },
|
|
13
|
+
{ name: 'IPv6 loopback', value: 'http://[::1]/' },
|
|
14
|
+
{ name: 'IPv6 localhost', value: 'http://[0:0:0:0:0:ffff:127.0.0.1]/' },
|
|
15
|
+
{ name: 'DNS rebinding (nip.io)', value: 'http://127.0.0.1.nip.io/' },
|
|
16
|
+
{ name: 'DNS rebinding (xip.io)', value: 'http://127.0.0.1.xip.io/' },
|
|
17
|
+
{ name: 'localhost variants', value: 'http://localhost/' },
|
|
18
|
+
{ name: 'localhost with port', value: 'http://localhost:22/' },
|
|
19
|
+
{ name: '0.0.0.0', value: 'http://0.0.0.0/' },
|
|
20
|
+
{ name: 'Double URL encode', value: 'http://%31%32%37%2e%30%2e%30%2e%31/' },
|
|
21
|
+
{ name: 'Unicode bypass', value: 'http://①②⑦.⓪.⓪.①/' },
|
|
22
|
+
{ name: 'File protocol', value: 'file:///etc/passwd' },
|
|
23
|
+
{ name: 'Gopher protocol', value: 'gopher://127.0.0.1:6379/' },
|
|
24
|
+
{ name: 'Dict protocol', value: 'dict://127.0.0.1:22/' },
|
|
25
|
+
{ name: 'Redirect chain', value: 'http://http-redirector.burpcollaborator.net/redirect?target=http://169.254.169.254/' },
|
|
26
|
+
{ name: 'Short URL', value: 'http://bit.ly/127-0-0-1' },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
for (const param of ssrfParams) {
|
|
30
|
+
for (const { name, value } of bypassPayloads.slice(0, 12)) {
|
|
31
|
+
try {
|
|
32
|
+
const resp = await fetchText(`${origin}/?${param}=${encodeURIComponent(value)}`, {}, 8000);
|
|
33
|
+
if (/root:x:0|ssh-|redis_version|ami-id|instance-id|Connection refused|SSH-|HTTP\/1\.[01]/.test(resp.body)) {
|
|
34
|
+
addFinding('CRITICAL', 'SSRF Bypass Chains', `SSRF bypass via ${name}: ?${param}=`, `Payload "${value}" returned internal service response`, 'Block all internal IP ranges. Normalize URLs before validation. Use egress rules and DNS-level blocking. Implement ALLOW-listing, not DENY-listing.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function scanJwtRemoteAdvanced(origin, spinner) {
|
|
43
|
+
spinner.text = 'Testing JWT advanced attacks (kid, JWK, none, key confusion)...';
|
|
44
|
+
|
|
45
|
+
const unprotectedPath = `eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.${btoa(JSON.stringify({ sub: 'admin', role: 'admin', exp: 9999999999 }))}.`;
|
|
46
|
+
|
|
47
|
+
const apiPaths = ['/api', '/api/me', '/api/user', '/api/profile', '/api/admin', '/dashboard'];
|
|
48
|
+
|
|
49
|
+
for (const path of apiPaths) {
|
|
50
|
+
try {
|
|
51
|
+
const resp = await fetchText(`${origin}${path}`, {
|
|
52
|
+
headers: { Authorization: `Bearer ${unprotectedPath}` },
|
|
53
|
+
}, 5000);
|
|
54
|
+
|
|
55
|
+
if (resp.status === 200) {
|
|
56
|
+
addFinding('CRITICAL', 'JWT Advanced', 'JWT "none" algorithm accepted', `${origin}${path} accepted unsigned JWT with alg=none`, 'Always enforce strict algorithm validation. Whitelist expected algorithms. Never accept "none" algorithm.');
|
|
57
|
+
break;
|
|
58
|
+
} else if (resp.status === 403 || resp.status === 401) {
|
|
59
|
+
addFinding('INFO', 'JWT Advanced', 'JWT rejected (good) at ' + path, `None algorithm rejected with ${resp.status}`, 'Proper algorithm enforcement detected. Ensure RS256 key confusion vector is also blocked.');
|
|
60
|
+
}
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const resp = await fetchText(`${origin}/.well-known/jwks.json`, {}, 5000);
|
|
66
|
+
if (resp.status === 200 && resp.body.includes('"keys"')) {
|
|
67
|
+
addFinding('INFO', 'JWT Advanced', 'JWKS endpoint found at /.well-known/jwks.json', 'JWK Set publicly exposed', 'Review JWKS for single key (private key leak risk). Ensure keys are rotated.');
|
|
68
|
+
}
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function scanGrpc(origin, spinner) {
|
|
73
|
+
spinner.text = 'Testing gRPC reflection and endpoints...';
|
|
74
|
+
|
|
75
|
+
const grpcEndpoints = ['/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo',
|
|
76
|
+
'/grpc.reflection.v1.ServerReflection/ServerReflectionInfo'];
|
|
77
|
+
|
|
78
|
+
for (const endpoint of grpcEndpoints) {
|
|
79
|
+
try {
|
|
80
|
+
const resp = await fetchText(`${origin}${endpoint}`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { 'Content-Type': 'application/grpc' },
|
|
83
|
+
}, 5000);
|
|
84
|
+
|
|
85
|
+
if (resp.status === 200 || resp.status === 415 || resp.status === 200) {
|
|
86
|
+
addFinding('MEDIUM', 'gRPC/OpenAPI', `gRPC reflection endpoint reachable: ${endpoint}`, `Server responded with status ${resp.status}`, 'Disable gRPC reflection in production. Use grpcurl to enumerate exposed services. Add authentication to gRPC endpoints.');
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
} catch {}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function scanOpenApi(origin, spinner) {
|
|
94
|
+
spinner.text = 'Fuzzing OpenAPI/Swagger schemas...';
|
|
95
|
+
|
|
96
|
+
const openApiPaths = ['/swagger.json', '/swagger.yaml', '/api-docs', '/openapi.json', '/openapi.yaml',
|
|
97
|
+
'/v1/openapi.json', '/v2/api-docs', '/v3/api-docs', '/api/openapi.json', '/docs/api', '/swagger-ui.html'];
|
|
98
|
+
|
|
99
|
+
for (const path of openApiPaths) {
|
|
100
|
+
try {
|
|
101
|
+
const resp = await fetchText(`${origin}${path}`, {}, 5000);
|
|
102
|
+
if (resp.status === 200 && (resp.body.includes('"openapi"') || resp.body.includes('"swagger"') || resp.body.includes('"paths"') || resp.body.includes('swagger'))) {
|
|
103
|
+
addFinding('HIGH', 'gRPC/OpenAPI', `OpenAPI/Swagger spec exposed: ${path}`, 'API schema publicly accessible - reveals all endpoints, parameters, and schemas', 'Restrict OpenAPI specs to authenticated internal users. Expose documentation only in dev environment.');
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
} catch {}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function scanWebrtc(origin, spinner) {
|
|
111
|
+
spinner.text = 'Testing WebRTC IP leakage...';
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const resp = await fetchText(origin);
|
|
115
|
+
if (resp.body.includes('RTCPeerConnection') || resp.body.includes('webkitRTCPeerConnection') || resp.body.includes('iceServers') || resp.body.includes('stun:') || resp.body.includes('turn:')) {
|
|
116
|
+
addFinding('LOW', 'Service Worker / WebRTC', 'WebRTC usage detected (IP leak risk)', 'ICE/STUN/TURN servers configured in page', 'WebRTC can leak internal IP addresses even behind VPN/proxy. Use WebRTC block extension or disable in browser settings for anonymity.');
|
|
117
|
+
} else {
|
|
118
|
+
addFinding('INFO', 'Service Worker / WebRTC', 'WebRTC IP leak check', 'No WebRTC detected on main page', 'Verify subpages and authenticated sections for WebRTC usage that could leak internal IPs.');
|
|
119
|
+
}
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function scanStoredDomXss(origin, spinner) {
|
|
124
|
+
spinner.text = 'Automated stored/DOM XSS with browser...';
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const resp = await fetchText(origin);
|
|
128
|
+
const body = resp.body;
|
|
129
|
+
|
|
130
|
+
const inputNames = [];
|
|
131
|
+
const inputPattern = /<input[^>]*name\s*=\s*['"]([^'"]+)['"]/gi;
|
|
132
|
+
let match;
|
|
133
|
+
while ((match = inputPattern.exec(body)) !== null) {
|
|
134
|
+
inputNames.push(match[1]);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const textareaPattern = /<textarea[^>]*name\s*=\s*['"]([^'"]+)['"]/gi;
|
|
138
|
+
while ((match = textareaPattern.exec(body)) !== null) {
|
|
139
|
+
inputNames.push(match[1]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const commentFields = inputNames.filter(n => /comment|message|body|content|text|description|bio|about|review|feedback/i.test(n));
|
|
143
|
+
const searchFields = inputNames.filter(n => /search|query|q|keyword|term/i.test(n));
|
|
144
|
+
|
|
145
|
+
if (commentFields.length > 0) {
|
|
146
|
+
addFinding('MEDIUM', 'Stored/DOM XSS', `Stored XSS target: ${commentFields.length} comment/input fields`, `Fields: ${commentFields.join(', ')} - test with <script>, <img onerror>, <svg/onload> payloads`, 'Submit XSS payloads into these fields, then visit the page where content is rendered without sanitization.');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (inputNames.length > 10) {
|
|
150
|
+
addFinding('INFO', 'Stored/DOM XSS', `${inputNames.length} input fields discovered for XSS testing`, `All fields: ${inputNames.join(', ').substring(0, 200)}`, 'Use browser-automated tools to inject and verify XSS in all input fields. Check for WAF bypass patterns.');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const windowEventPattern = /window\.addEventListener\s*\(\s*['"]message['"]/g;
|
|
154
|
+
if (windowEventPattern.test(body)) {
|
|
155
|
+
addFinding('MEDIUM', 'Stored/DOM XSS', 'postMessage listener detected (DOM XSS via messaging)', 'Page has message event listener - test for origin validation bypass', 'Ensure postMessage handler validates event.origin before processing event.data. Never assign untrusted data to innerHTML via postMessage.');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const urlHashPattern = /location\.(?:hash|search)\s*(?:=|(?:\+|concat|includes|split|substring))/g;
|
|
159
|
+
if (urlHashPattern.test(body)) {
|
|
160
|
+
addFinding('MEDIUM', 'Stored/DOM XSS', 'URL hash/search used in JavaScript (DOM XSS source)', 'URL fragment/search processed in client code', 'Sanitize URL hash/search before using in DOM manipulation. Avoid assigning location.hash to innerHTML.');
|
|
161
|
+
}
|
|
162
|
+
} catch {}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function scanSsiRemote(origin, spinner) {
|
|
166
|
+
spinner.text = 'Testing Server-Side Includes (SSI)...';
|
|
167
|
+
const ssiPaths = ['/index.shtml', '/test.shtml', '/index.stm', '/includes/header.shtml'];
|
|
168
|
+
|
|
169
|
+
for (const path of ssiPaths) {
|
|
170
|
+
try {
|
|
171
|
+
const resp = await fetchText(`${origin}${path}`, { headers: { 'User-Agent': '<!--#echo var="DATE_LOCAL" -->' } }, 5000);
|
|
172
|
+
|
|
173
|
+
if (resp.status === 200 && resp.body.length > 0) {
|
|
174
|
+
if (/<!--#|Server Side Include|SSI/i.test(resp.body) || resp.body.includes('DATE_LOCAL')) {
|
|
175
|
+
addFinding('MEDIUM', 'XPath / SSI', `SSI endpoint found: ${path}`, `Potential Server-Side Includes at ${origin}${path}`, 'Disable SSI if not needed. If needed, never pass user input to SSI directives.');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch {}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function scanXpathRemote(origin, spinner) {
|
|
183
|
+
spinner.text = 'Testing XPath injection...';
|
|
184
|
+
|
|
185
|
+
const xpathPayloads = [
|
|
186
|
+
{ name: 'Auth bypass', payload: "' or '1'='1", param: 'username' },
|
|
187
|
+
{ name: 'XPath OR injection', payload: "' or 1=1 or 'a'='a", param: 'user' },
|
|
188
|
+
{ name: 'String extraction', payload: "' and string-length(//user[name/text()='admin']/password/text())=4 and '1'='1", param: 'id' },
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
for (const { name, payload, param } of xpathPayloads) {
|
|
192
|
+
try {
|
|
193
|
+
const resp = await fetchText(`${origin}/api/login`, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: { 'Content-Type': 'application/xml' },
|
|
196
|
+
body: `<login><username>${payload}</username><password>test</password></login>`,
|
|
197
|
+
}, 5000);
|
|
198
|
+
|
|
199
|
+
if (resp.status === 200) {
|
|
200
|
+
addFinding('CRITICAL', 'XPath / SSI', `XPath injection via ${param}: ${name}`, `Payload: ${payload} returned success`, 'Use parameterized XPath queries. Sanitize input against XPath special characters. Consider using JSON/NoSQL instead of XML.');
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
} catch {}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function scanTimingRemote(origin, spinner) {
|
|
208
|
+
spinner.text = 'Timing side-channel probe...';
|
|
209
|
+
|
|
210
|
+
const validUser = 'admin@target.com';
|
|
211
|
+
const invalidUser = `nonexistent${Date.now()}@random.com`;
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const startValid = Date.now();
|
|
215
|
+
await fetchText(`${origin}/api/login`, {
|
|
216
|
+
method: 'POST',
|
|
217
|
+
headers: { 'Content-Type': 'application/json' },
|
|
218
|
+
body: JSON.stringify({ email: validUser, password: 'wrong' }),
|
|
219
|
+
}, 8000);
|
|
220
|
+
const validTime = Date.now() - startValid;
|
|
221
|
+
|
|
222
|
+
const startInvalid = Date.now();
|
|
223
|
+
await fetchText(`${origin}/api/login`, {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
headers: { 'Content-Type': 'application/json' },
|
|
226
|
+
body: JSON.stringify({ email: invalidUser, password: 'wrong' }),
|
|
227
|
+
}, 8000);
|
|
228
|
+
const invalidTime = Date.now() - startInvalid;
|
|
229
|
+
|
|
230
|
+
if (Math.abs(validTime - invalidTime) > 200) {
|
|
231
|
+
addFinding('HIGH', 'Timing Side-Channel', `Timing difference detected: ${Math.abs(validTime - invalidTime)}ms`, `Valid user took ${validTime}ms, invalid took ${invalidTime}ms (difference: ${Math.abs(validTime - invalidTime)}ms)`, 'Use constant-time comparison. Ensure login endpoint responds identically for existing and non-existing users. Add random jitter.');
|
|
232
|
+
} else {
|
|
233
|
+
addFinding('INFO', 'Timing Side-Channel', 'Login timing appears consistent', `Valid: ${validTime}ms, Invalid: ${invalidTime}ms (diff: ${Math.abs(validTime - invalidTime)}ms)`, 'Good practice. Continue monitoring for timing differences in other endpoints.');
|
|
234
|
+
}
|
|
235
|
+
} catch {}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function btoa(str) {
|
|
239
|
+
return Buffer.from(str).toString('base64').replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
240
|
+
}
|