redgun-security 1.2.0 → 1.3.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/package.json +1 -1
- package/scan.js +10 -0
- package/src/local/csti.js +71 -0
- package/src/local/index.js +12 -0
- package/src/local/jwt-advanced.js +70 -0
- package/src/local/padding-oracle.js +66 -0
- package/src/local/service-worker.js +64 -0
- package/src/local/timing.js +66 -0
- package/src/local/xpath-ssi.js +67 -0
- package/src/remote/complete.js +240 -0
package/package.json
CHANGED
package/scan.js
CHANGED
|
@@ -5,6 +5,7 @@ import { runCrawler } from './src/remote/crawler.js';
|
|
|
5
5
|
import { runProbe } from './src/remote/probe.js';
|
|
6
6
|
import { scanXxeRemote, scanOauthRemote, scanAccessControlRemote, scanWebCacheDeception, scanParameterPollution, scanFileUpload, scanDomBased, scanHttp2 } from './src/remote/portswigger.js';
|
|
7
7
|
import { scanSamlRemote, scanLdapRemote, scanMfaBypass, scanWebsocketReplay, scanPasswordReset, scanCsrfRemote, scanDanglingDns, scanCloudRemote } from './src/remote/advanced.js';
|
|
8
|
+
import { scanSsrfBypassChains, scanJwtRemoteAdvanced, scanGrpc, scanOpenApi, scanWebrtc, scanStoredDomXss, scanSsiRemote, scanXpathRemote, scanTimingRemote } from './src/remote/complete.js';
|
|
8
9
|
|
|
9
10
|
export async function runRemoteScan(url, spinner, modules = null) {
|
|
10
11
|
const target = new URL(url);
|
|
@@ -55,6 +56,15 @@ export async function runRemoteScan(url, spinner, modules = null) {
|
|
|
55
56
|
{ name: 'CSRF Token Analysis (remote)', value: 'csrf', fn: () => scanCsrfRemote(origin, spinner) },
|
|
56
57
|
{ name: 'Subdomain Takeover (Dangling DNS)', value: 'takeover', fn: () => scanDanglingDns(hostname, spinner) },
|
|
57
58
|
{ name: 'Cloud Metadata SSRF', value: 'cloudmeta', fn: () => scanCloudRemote(origin, spinner) },
|
|
59
|
+
{ name: 'SSRF Bypass Chains', value: 'ssrfbypass', fn: () => scanSsrfBypassChains(origin, spinner) },
|
|
60
|
+
{ name: 'JWT Advanced (kid/none/JWK)', value: 'jwtadv', fn: () => scanJwtRemoteAdvanced(origin, spinner) },
|
|
61
|
+
{ name: 'gRPC Reflection', value: 'grpc', fn: () => scanGrpc(origin, spinner) },
|
|
62
|
+
{ name: 'OpenAPI/Swagger Fuzz', value: 'openapi', fn: () => scanOpenApi(origin, spinner) },
|
|
63
|
+
{ name: 'WebRTC IP Leak', value: 'webrtc', fn: () => scanWebrtc(origin, spinner) },
|
|
64
|
+
{ name: 'Stored/DOM XSS Auto', value: 'storedxss', fn: () => scanStoredDomXss(origin, spinner) },
|
|
65
|
+
{ name: 'SSI Injection Remote', value: 'ssi', fn: () => scanSsiRemote(origin, spinner) },
|
|
66
|
+
{ name: 'XPath Injection Remote', value: 'xpath', fn: () => scanXpathRemote(origin, spinner) },
|
|
67
|
+
{ name: 'Timing Side-Channel', value: 'timing', fn: () => scanTimingRemote(origin, spinner) },
|
|
58
68
|
];
|
|
59
69
|
|
|
60
70
|
const toRun = modules ? allModules.filter((m) => modules.includes(m.value)) : allModules;
|
|
@@ -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', '.py', '.rb', '.php', '.go', '.java'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
|
|
7
|
+
|
|
8
|
+
export async function auditCsti(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for client-side template 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: /\{\{.+\}\}/g, name: 'AngularJS double-curly expression', severity: 'INFO' },
|
|
20
|
+
{ pattern: /ng-app|ng-controller|ng-bind-html|ng-non-bindable/gi, name: 'AngularJS bindings detected', severity: 'INFO' },
|
|
21
|
+
{ pattern: /\$sce\.trustAsHtml|\$sceProvider\.enabled\s*\(\s*false/gi, name: 'AngularJS SCE disabled/untrusted HTML', severity: 'HIGH' },
|
|
22
|
+
{ pattern: /ng-bind-html\s*=\s*(?:req|request|params|query|body)/gi, name: 'AngularJS ng-bind-html with user input', severity: 'HIGH' },
|
|
23
|
+
{ pattern: /angular\.module.*run\s*\(/gi, name: 'AngularJS module detected', severity: 'INFO' },
|
|
24
|
+
{ pattern: /v-html\s*=\s*(?:req|params|query|body)/gi, name: 'Vue v-html with user input', severity: 'HIGH' },
|
|
25
|
+
{ pattern: /v-bind:src|:src\s*=\s*['"`]\{\{/gi, name: 'Vue dynamic src binding', severity: 'MEDIUM' },
|
|
26
|
+
{ pattern: /Vue\.compile\s*\(|new\s+Vue\s*\(|createApp\s*\(/gi, name: 'Vue instance detected', severity: 'INFO' },
|
|
27
|
+
{ pattern: /React\.createElement|ReactDOM\.render|createRoot\s*\(/gi, name: 'React rendering detected', severity: 'INFO' },
|
|
28
|
+
{ pattern: /dangerouslySetInnerHTML\s*:\s*\{\s*__html\s*:\s*(?:req|params|query|body)/gi, name: 'React dangerouslySetInnerHTML with user input', severity: 'HIGH' },
|
|
29
|
+
{ pattern: /Svelte.*\$set|Svelte.*\$\$invalidate|Svelte.*dangerously/gi, name: 'Svelte dynamic updates', severity: 'MEDIUM' },
|
|
30
|
+
{ pattern: /TemplateRef|ViewContainerRef|ComponentFactoryResolver/gi, name: 'Angular dynamic component (XSS surface)', severity: 'MEDIUM' },
|
|
31
|
+
{ pattern: /bypassSecurityTrustHtml\s*\(/gi, name: 'Angular bypassSecurityTrustHtml', severity: 'HIGH' },
|
|
32
|
+
{ pattern: /bypassSecurityTrustScript|bypassSecurityTrustResourceUrl/gi, name: 'Angular bypassSecurityTrust (unsafe)', severity: 'HIGH' },
|
|
33
|
+
{ pattern: /ElementRef\.nativeElement\.innerHTML/gi, name: 'Angular ElementRef innerHTML', severity: 'MEDIUM' },
|
|
34
|
+
{ pattern: /sanitizeHtml|DOMPurify\.sanitize/gi, name: 'HTML sanitization used (good)', severity: 'INFO' },
|
|
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('#') || line.startsWith('*')) continue;
|
|
43
|
+
|
|
44
|
+
addFinding(
|
|
45
|
+
severity,
|
|
46
|
+
'Client-Side Template Injection (CSTI)',
|
|
47
|
+
name,
|
|
48
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
49
|
+
'Never bind user input to v-html/dangerouslySetInnerHTML/ng-bind-html. Use DOMPurify for sanitization. Avoid bypassSecurityTrust* APIs. Use Angular default sanitizer. For Vue, use v-text instead of v-html.'
|
|
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
|
+
}
|
package/src/local/index.js
CHANGED
|
@@ -24,6 +24,12 @@ import { auditCloud } from './cloud.js';
|
|
|
24
24
|
import { auditCicd } from './cicd.js';
|
|
25
25
|
import { auditMobile } from './mobile.js';
|
|
26
26
|
import { auditWeb3 } from './web3.js';
|
|
27
|
+
import { auditXpathSsi } from './xpath-ssi.js';
|
|
28
|
+
import { auditTiming } from './timing.js';
|
|
29
|
+
import { auditJwtAdvanced } from './jwt-advanced.js';
|
|
30
|
+
import { auditCsti } from './csti.js';
|
|
31
|
+
import { auditServiceWorker } from './service-worker.js';
|
|
32
|
+
import { auditPaddingOracle } from './padding-oracle.js';
|
|
27
33
|
|
|
28
34
|
export const LOCAL_MODULES = [
|
|
29
35
|
{ name: 'Code Secrets', value: 'secrets', fn: auditSecrets },
|
|
@@ -52,6 +58,12 @@ export const LOCAL_MODULES = [
|
|
|
52
58
|
{ name: 'CI/CD Pipeline', value: 'cicd', fn: auditCicd },
|
|
53
59
|
{ name: 'Mobile Security', value: 'mobile', fn: auditMobile },
|
|
54
60
|
{ name: 'Web3 / Smart Contracts', value: 'web3', fn: auditWeb3 },
|
|
61
|
+
{ name: 'XPath / SSI Injection', value: 'xpath', fn: auditXpathSsi },
|
|
62
|
+
{ name: 'Timing Side-Channels', value: 'timing', fn: auditTiming },
|
|
63
|
+
{ name: 'JWT Advanced (kid, JWK, jku)', value: 'jwtadv', fn: auditJwtAdvanced },
|
|
64
|
+
{ name: 'Client-Side Template Injection (CSTI)', value: 'csti', fn: auditCsti },
|
|
65
|
+
{ name: 'Service Worker / WebRTC', value: 'sworker', fn: auditServiceWorker },
|
|
66
|
+
{ name: 'Padding / Compression Oracle', value: 'padding', fn: auditPaddingOracle },
|
|
55
67
|
];
|
|
56
68
|
|
|
57
69
|
export async function runLocalAudit(projectPath, spinner, modules = null) {
|
|
@@ -0,0 +1,70 @@
|
|
|
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', '.json', '.env'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
|
|
7
|
+
|
|
8
|
+
export async function auditJwtAdvanced(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for advanced JWT attacks (kid, JWK, jku)...';
|
|
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: /(?:kid|keyId|key_id)\s*[:=]\s*(?:req|request|params|query|body|input)/gi, name: 'Key ID (kid) from user input - path traversal/LFI attack', severity: 'CRITICAL' },
|
|
20
|
+
{ pattern: /jwk\s*[:=]\s*(?:req|request|params|query|body)/gi, name: 'JWK from user input - key injection', severity: 'CRITICAL' },
|
|
21
|
+
{ pattern: /jku\s*[:=]\s*(?:req|request|params|query|body)/gi, name: 'jku (JWK Set URL) from user input - SSRF', severity: 'CRITICAL' },
|
|
22
|
+
{ pattern: /x5u\s*[:=]\s*(?:req|request|params|query|body)/gi, name: 'x5u URL from user input', severity: 'CRITICAL' },
|
|
23
|
+
{ pattern: /x5c\s*[:=]\s*(?:req|request|params|query|body)/gi, name: 'x5c certificate from user input', severity: 'CRITICAL' },
|
|
24
|
+
{ pattern: /algorithm\s*[:=]\s*['"]none['"]|alg\s*:\s*['"]none['"]/gi, name: 'Algorithm "none" accepted', severity: 'CRITICAL' },
|
|
25
|
+
{ pattern: /algorithms\s*:\s*\[.*['"]HS256['"].*\]|algorithm\s*:\s*['"]HS256['"]/gi, name: 'HS256 algorithm (key confusion vector)', severity: 'HIGH' },
|
|
26
|
+
{ pattern: /(?:verify|validate)\s*\(\s*(?:token|jwt)\s*,\s*(?:secret|key)\s*,?\s*\{[^}]*algorithms?\s*:\s*\[/gi, name: 'JWT verify with algorithm whitelist', severity: 'INFO' },
|
|
27
|
+
{ pattern: /jwt\.(?:decode|sign|verify).*complete\s*:\s*true/gi, name: 'jwt.io format (complete decode)', severity: 'LOW' },
|
|
28
|
+
{ pattern: /jsonwebtoken|jose|jwk-to-pem|jwt-simple|njwt/gi, name: 'JWT library usage', severity: 'INFO' },
|
|
29
|
+
{ pattern: /(?:publicKey|public_key|pubkey)\s*[:=]\s*(?:req|request|params|query|body)/gi, name: 'Public key from user input (key confusion)', severity: 'CRITICAL' },
|
|
30
|
+
{ pattern: /(?:jwt|token|bearer).*(?:header|payload)\s*.*(?:decode|parse|split|extract)\s*\(/gi, name: 'JWT header/payload parsing (check algorithm handling)', severity: 'MEDIUM' },
|
|
31
|
+
{ pattern: /jwt\.sign\s*\(\s*[^,]*,\s*['"][a-zA-Z0-9]{1,16}['"]/gi, name: 'Weak JWT signing secret (<16 chars)', severity: 'HIGH' },
|
|
32
|
+
{ pattern: /(?:secret|jwtSecret|JWT_SECRET)\s*=\s*['"]?[a-zA-Z0-9]{1,24}['"]?/gi, name: 'Short JWT secret in code', severity: 'HIGH' },
|
|
33
|
+
{ pattern: /crypto\.createPublicKey|crypto\.createPrivateKey/gi, name: 'crypto key creation (check source)', severity: 'INFO' },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
for (const { pattern, name, severity } of patterns) {
|
|
37
|
+
let match;
|
|
38
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
39
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
40
|
+
const line = lines[lineNum - 1]?.trim() || '';
|
|
41
|
+
if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*')) continue;
|
|
42
|
+
|
|
43
|
+
addFinding(
|
|
44
|
+
severity,
|
|
45
|
+
'JWT Advanced Attacks',
|
|
46
|
+
name,
|
|
47
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
48
|
+
'Whitelist allowed algorithms (e.g., ["RS256"]). Never use "none" algorithm. Do not accept kid/jwk/jku/x5u from untrusted input. Use asymmetric signing (RS256/ES256). Validate kid against a trusted key store, not a file path. Check for key confusion: if using RS256 ensure public key is not accepted as HMAC secret.'
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getFiles(dir, files = []) {
|
|
57
|
+
try {
|
|
58
|
+
const entries = readdirSync(dir);
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
|
|
61
|
+
const fullPath = join(dir, entry);
|
|
62
|
+
try {
|
|
63
|
+
const stat = statSync(fullPath);
|
|
64
|
+
if (stat.isDirectory()) getFiles(fullPath, files);
|
|
65
|
+
else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
} catch {}
|
|
69
|
+
return files;
|
|
70
|
+
}
|
|
@@ -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', '.c', '.cpp', '.cs'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
|
|
7
|
+
|
|
8
|
+
export async function auditPaddingOracle(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for padding/comression oracle 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: /(?:decrypt|decipher|unpad)\s*\([^)]*\)\s*(?:catch|if.*error)/gi, name: 'Decryption with error handling (padding oracle)', severity: 'HIGH' },
|
|
20
|
+
{ pattern: /(?:padding|decrypt)\s*(?:bad|invalid|wrong|error|fail)/gi, name: 'Decryption error message (padding oracle indicator)', severity: 'HIGH' },
|
|
21
|
+
{ pattern: /CBC|cipher\.final|cipher\.update/gi, name: 'CBC mode encryption (padding oracle vector)', severity: 'MEDIUM' },
|
|
22
|
+
{ pattern: /createDecipheriv\s*\(\s*['"](?:aes|des)-.*-(?:cbc|ecb)/gi, name: 'CBC/ECB mode decrypt (potential padding oracle)', severity: 'MEDIUM' },
|
|
23
|
+
{ pattern: /(?:error|exception|err)\s*\(\s*['"](?:bad|invalid|wrong) (?:padding|decrypt|cipher)/gi, name: 'Padding error in response', severity: 'HIGH' },
|
|
24
|
+
{ pattern: /OAEP|RSASSA-PSS|RSAES-OAEP/gi, name: 'RSA OAEP padding (good)', severity: 'INFO' },
|
|
25
|
+
{ pattern: /PKCS|pkcs/i, name: 'PKCS padding reference', severity: 'INFO' },
|
|
26
|
+
{ pattern: /gzip|deflate|compress|zlib|Content-Encoding/gi, name: 'Compression usage (CRIME/BREACH vector)', severity: 'LOW' },
|
|
27
|
+
{ pattern: /(?:token|session|cookie|jwt).*(?:compress|gzip|deflate)/gi, name: 'Compression of secrets/tokens (CRIME/BREACH)', severity: 'MEDIUM' },
|
|
28
|
+
{ pattern: /(?:https|tls|ssl).*(?:compress|gzip|deflate)/gi, name: 'TLS compression reference (CRIME)', severity: 'MEDIUM' },
|
|
29
|
+
{ pattern: /\bcrypto\.createDecipheriv\b/gi, name: 'Node.js createDecipheriv (check error handling)', severity: 'MEDIUM' },
|
|
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
|
+
'Padding / Compression Oracle',
|
|
42
|
+
name,
|
|
43
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
44
|
+
'Use authenticated encryption (AES-GCM, ChaCha20-Poly1305) instead of CBC. Never leak decryption errors to clients. Add random MAC before encrypting. Disable TLS compression. Avoid compressing response bodies that contain secrets or CSRF tokens.'
|
|
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,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,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
|
+
}
|