redgun-security 1.1.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/README.md +0 -1
- package/bin/redgun.js +1 -1
- package/package.json +1 -1
- package/scan.js +19 -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/csti.js +71 -0
- package/src/local/index.js +36 -8
- package/src/local/jwt-advanced.js +70 -0
- package/src/local/ldap.js +67 -0
- package/src/local/mobile.js +71 -0
- package/src/local/padding-oracle.js +66 -0
- package/src/local/saml.js +72 -0
- package/src/local/service-worker.js +64 -0
- package/src/local/timing.js +66 -0
- package/src/local/web3.js +73 -0
- package/src/local/xpath-ssi.js +67 -0
- package/src/remote/advanced.js +238 -0
- package/src/remote/complete.js +240 -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,8 @@ 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';
|
|
8
|
+
import { scanSsrfBypassChains, scanJwtRemoteAdvanced, scanGrpc, scanOpenApi, scanWebrtc, scanStoredDomXss, scanSsiRemote, scanXpathRemote, scanTimingRemote } from './src/remote/complete.js';
|
|
7
9
|
|
|
8
10
|
export async function runRemoteScan(url, spinner, modules = null) {
|
|
9
11
|
const target = new URL(url);
|
|
@@ -46,6 +48,23 @@ export async function runRemoteScan(url, spinner, modules = null) {
|
|
|
46
48
|
{ name: 'File Upload Testing (PortSwigger)', value: 'upload', fn: () => scanFileUpload(origin, spinner) },
|
|
47
49
|
{ name: 'DOM-Based Vulnerabilities (PortSwigger)', value: 'dom', fn: () => scanDomBased(origin, spinner) },
|
|
48
50
|
{ name: 'HTTP/2 Attacks (PortSwigger)', value: 'h2', fn: () => scanHttp2(origin, spinner) },
|
|
51
|
+
{ name: 'SAML/SSO Attacks', value: 'saml', fn: () => scanSamlRemote(origin, spinner) },
|
|
52
|
+
{ name: 'LDAP Injection', value: 'ldap', fn: () => scanLdapRemote(origin, spinner) },
|
|
53
|
+
{ name: 'MFA Bypass Testing', value: 'mfa', fn: () => scanMfaBypass(origin, spinner) },
|
|
54
|
+
{ name: 'WebSocket Replay/CSWSH', value: 'wshijack', fn: () => scanWebsocketReplay(origin, spinner) },
|
|
55
|
+
{ name: 'Password Reset Security', value: 'pwdreset', fn: () => scanPasswordReset(origin, spinner) },
|
|
56
|
+
{ name: 'CSRF Token Analysis (remote)', value: 'csrf', fn: () => scanCsrfRemote(origin, spinner) },
|
|
57
|
+
{ name: 'Subdomain Takeover (Dangling DNS)', value: 'takeover', fn: () => scanDanglingDns(hostname, spinner) },
|
|
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) },
|
|
49
68
|
];
|
|
50
69
|
|
|
51
70
|
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
|
+
}
|
|
@@ -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
|
@@ -16,6 +16,20 @@ 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';
|
|
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';
|
|
19
33
|
|
|
20
34
|
export const LOCAL_MODULES = [
|
|
21
35
|
{ name: 'Code Secrets', value: 'secrets', fn: auditSecrets },
|
|
@@ -24,18 +38,32 @@ export const LOCAL_MODULES = [
|
|
|
24
38
|
{ name: 'Code Vulnerabilities (SQLi, XSS)', value: 'codevuln', fn: auditCodeVulnerabilities },
|
|
25
39
|
{ name: 'Auth & Middleware', value: 'auth', fn: auditAuth },
|
|
26
40
|
{ 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
|
|
41
|
+
{ name: 'SSRF Detection', value: 'ssrf', fn: auditSsrf },
|
|
42
|
+
{ name: 'SSTI Detection', value: 'ssti', fn: auditSsti },
|
|
43
|
+
{ name: 'Insecure Deserialization', value: 'deser', fn: auditDeserialization },
|
|
44
|
+
{ name: 'Prototype Pollution', value: 'proto', fn: auditPrototypePollution },
|
|
45
|
+
{ name: 'JWT Vulnerabilities', value: 'jwt', fn: auditJwt },
|
|
46
|
+
{ name: 'Path Traversal / LFI', value: 'lfi', fn: auditPathTraversal },
|
|
47
|
+
{ name: 'Command Injection', value: 'cmdi', fn: auditCommandInjection },
|
|
48
|
+
{ name: 'Weak Cryptography', value: 'crypto', fn: auditCrypto },
|
|
35
49
|
{ name: 'XXE - XML External Entity (PortSwigger)', value: 'xxe', fn: auditXxe },
|
|
36
50
|
{ name: 'Access Control / IDOR (PortSwigger)', value: 'idor', fn: auditAccessControl },
|
|
37
51
|
{ name: 'OAuth / OIDC Flaws (PortSwigger)', value: 'oauth', fn: auditOauth },
|
|
38
52
|
{ name: 'Business Logic Flaws (PortSwigger)', value: 'bizlogic', fn: auditBusinessLogic },
|
|
53
|
+
{ name: 'SAML / SSO Attacks', value: 'saml', fn: auditSaml },
|
|
54
|
+
{ name: 'LDAP Injection', value: 'ldap', fn: auditLdap },
|
|
55
|
+
{ name: 'CSRF Token Analysis', value: 'csrf', fn: auditCsrf },
|
|
56
|
+
{ name: 'Account Takeover (ATO)', value: 'ato', fn: auditAto },
|
|
57
|
+
{ name: 'Cloud Misconfig (S3/IAM)', value: 'cloud', fn: auditCloud },
|
|
58
|
+
{ name: 'CI/CD Pipeline', value: 'cicd', fn: auditCicd },
|
|
59
|
+
{ name: 'Mobile Security', value: 'mobile', fn: auditMobile },
|
|
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 },
|
|
39
67
|
];
|
|
40
68
|
|
|
41
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
|
+
}
|