ship-safe 3.2.0 → 4.0.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 +182 -459
- package/cli/agents/api-fuzzer.js +224 -0
- package/cli/agents/auth-bypass-agent.js +326 -0
- package/cli/agents/base-agent.js +240 -0
- package/cli/agents/cicd-scanner.js +200 -0
- package/cli/agents/config-auditor.js +413 -0
- package/cli/agents/git-history-scanner.js +167 -0
- package/cli/agents/html-reporter.js +363 -0
- package/cli/agents/index.js +56 -0
- package/cli/agents/injection-tester.js +401 -0
- package/cli/agents/llm-redteam.js +251 -0
- package/cli/agents/mobile-scanner.js +225 -0
- package/cli/agents/orchestrator.js +152 -0
- package/cli/agents/policy-engine.js +149 -0
- package/cli/agents/recon-agent.js +196 -0
- package/cli/agents/sbom-generator.js +176 -0
- package/cli/agents/scoring-engine.js +207 -0
- package/cli/agents/ssrf-prober.js +130 -0
- package/cli/agents/supply-chain-agent.js +274 -0
- package/cli/bin/ship-safe.js +83 -3
- package/cli/commands/audit.js +565 -0
- package/cli/commands/red-team.js +315 -0
- package/cli/commands/watch.js +160 -0
- package/cli/index.js +36 -1
- package/cli/providers/llm-provider.js +288 -0
- package/package.json +18 -14
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* APIFuzzer Agent
|
|
3
|
+
* ================
|
|
4
|
+
*
|
|
5
|
+
* Static analysis of API endpoints for security anti-patterns.
|
|
6
|
+
* Checks authentication, authorization, input validation,
|
|
7
|
+
* rate limiting, CORS, error handling, data exposure,
|
|
8
|
+
* mass assignment, GraphQL, file uploads, and pagination.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { BaseAgent, createFinding } from './base-agent.js';
|
|
13
|
+
|
|
14
|
+
const PATTERNS = [
|
|
15
|
+
// ── Missing Authentication ─────────────────────────────────────────────────
|
|
16
|
+
{
|
|
17
|
+
rule: 'API_NO_AUTH_CHECK',
|
|
18
|
+
title: 'API: Route Without Auth Check',
|
|
19
|
+
regex: /(?:app|router)\.(?:post|put|patch|delete)\s*\(\s*['"][^'"]+['"]\s*,\s*(?:async\s+)?(?:\(req|function)/g,
|
|
20
|
+
severity: 'high',
|
|
21
|
+
cwe: 'CWE-306',
|
|
22
|
+
owasp: 'A07:2021',
|
|
23
|
+
confidence: 'medium',
|
|
24
|
+
description: 'State-changing API route without visible auth middleware. Verify authentication is enforced.',
|
|
25
|
+
fix: 'Add auth middleware: router.post("/api/data", authMiddleware, handler)',
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
// ── Input Validation ───────────────────────────────────────────────────────
|
|
29
|
+
{
|
|
30
|
+
rule: 'API_NO_VALIDATION',
|
|
31
|
+
title: 'API: No Input Validation',
|
|
32
|
+
regex: /(?:req\.body|request\.body|ctx\.request\.body)\s*(?:;|\))/g,
|
|
33
|
+
severity: 'medium',
|
|
34
|
+
cwe: 'CWE-20',
|
|
35
|
+
owasp: 'A03:2021',
|
|
36
|
+
confidence: 'low',
|
|
37
|
+
description: 'Request body used without validation. Validate with Zod, Joi, or Yup.',
|
|
38
|
+
fix: 'Validate: const data = schema.parse(req.body) using Zod or similar',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
rule: 'API_SPREAD_BODY',
|
|
42
|
+
title: 'API: Spread Request Body into Operation',
|
|
43
|
+
regex: /\.\.\.\s*(?:req\.body|request\.body|ctx\.request\.body)/g,
|
|
44
|
+
severity: 'high',
|
|
45
|
+
cwe: 'CWE-915',
|
|
46
|
+
owasp: 'A01:2021',
|
|
47
|
+
description: 'Spreading request body enables mass assignment. Only use allowed fields.',
|
|
48
|
+
fix: 'Destructure specific fields: const { name, email } = req.body',
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// ── Error Handling ─────────────────────────────────────────────────────────
|
|
52
|
+
{
|
|
53
|
+
rule: 'API_STACK_TRACE_RESPONSE',
|
|
54
|
+
title: 'API: Stack Trace in Response',
|
|
55
|
+
regex: /(?:res\.(?:json|send|status)|ctx\.body)\s*\(\s*(?:\{[^}]*(?:err\.stack|error\.stack|err\.message|error\.message)|err\b|error\b)/g,
|
|
56
|
+
severity: 'medium',
|
|
57
|
+
cwe: 'CWE-209',
|
|
58
|
+
owasp: 'A05:2021',
|
|
59
|
+
description: 'Error details sent in API response leak internal information.',
|
|
60
|
+
fix: 'Log errors server-side. Return generic message: res.status(500).json({ error: "Internal error" })',
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// ── Data Exposure ──────────────────────────────────────────────────────────
|
|
64
|
+
{
|
|
65
|
+
rule: 'API_EXCESSIVE_DATA',
|
|
66
|
+
title: 'API: Returning Full Database Object',
|
|
67
|
+
regex: /(?:res\.json|res\.send|ctx\.body)\s*\(\s*(?:user|users|record|result|data|row|document)\s*\)/g,
|
|
68
|
+
severity: 'medium',
|
|
69
|
+
cwe: 'CWE-200',
|
|
70
|
+
owasp: 'A01:2021',
|
|
71
|
+
confidence: 'low',
|
|
72
|
+
description: 'Returning full DB objects may expose sensitive fields (password, email, etc.).',
|
|
73
|
+
fix: 'Select specific fields: res.json({ id: user.id, name: user.name })',
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// ── File Upload ────────────────────────────────────────────────────────────
|
|
77
|
+
{
|
|
78
|
+
rule: 'API_UNRESTRICTED_UPLOAD',
|
|
79
|
+
title: 'API: Unrestricted File Upload',
|
|
80
|
+
regex: /(?:multer|formidable|busboy|multiparty)\s*\(/g,
|
|
81
|
+
severity: 'medium',
|
|
82
|
+
cwe: 'CWE-434',
|
|
83
|
+
owasp: 'A04:2021',
|
|
84
|
+
confidence: 'low',
|
|
85
|
+
description: 'File upload handler detected. Ensure file type validation, size limits, and secure storage.',
|
|
86
|
+
fix: 'Add: fileFilter, limits: { fileSize: 5*1024*1024 }, and validate MIME type',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
rule: 'API_UPLOAD_NO_TYPE_CHECK',
|
|
90
|
+
title: 'API: File Upload Without Type Validation',
|
|
91
|
+
regex: /(?:originalname|filename)\s*(?:\)|;)/g,
|
|
92
|
+
severity: 'high',
|
|
93
|
+
cwe: 'CWE-434',
|
|
94
|
+
owasp: 'A04:2021',
|
|
95
|
+
confidence: 'low',
|
|
96
|
+
description: 'File upload using original filename without type validation.',
|
|
97
|
+
fix: 'Validate file extension and MIME type. Generate random filenames for storage.',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
rule: 'API_PATH_IN_FILENAME',
|
|
101
|
+
title: 'API: Path Traversal in File Upload',
|
|
102
|
+
regex: /path\.join\s*\([^)]*(?:originalname|filename|req\.file|req\.body)/g,
|
|
103
|
+
severity: 'critical',
|
|
104
|
+
cwe: 'CWE-22',
|
|
105
|
+
owasp: 'A01:2021',
|
|
106
|
+
description: 'User-supplied filename in path construction enables directory traversal.',
|
|
107
|
+
fix: 'Generate random filename: crypto.randomUUID() + path.extname(file.originalname)',
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// ── GraphQL Security ───────────────────────────────────────────────────────
|
|
111
|
+
{
|
|
112
|
+
rule: 'GRAPHQL_INTROSPECTION',
|
|
113
|
+
title: 'GraphQL: Introspection Enabled',
|
|
114
|
+
regex: /introspection\s*:\s*true/g,
|
|
115
|
+
severity: 'medium',
|
|
116
|
+
cwe: 'CWE-200',
|
|
117
|
+
owasp: 'A05:2021',
|
|
118
|
+
description: 'GraphQL introspection enabled. Exposes full schema to attackers in production.',
|
|
119
|
+
fix: 'Disable in production: introspection: process.env.NODE_ENV !== "production"',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
rule: 'GRAPHQL_NO_DEPTH_LIMIT',
|
|
123
|
+
title: 'GraphQL: No Query Depth Limit',
|
|
124
|
+
regex: /(?:ApolloServer|GraphQLServer|createYoga|makeExecutableSchema)\s*\(/g,
|
|
125
|
+
severity: 'medium',
|
|
126
|
+
cwe: 'CWE-400',
|
|
127
|
+
confidence: 'low',
|
|
128
|
+
description: 'GraphQL server without query depth limiting. Enables deeply nested DoS queries.',
|
|
129
|
+
fix: 'Add depth limiting: graphql-depth-limit or @escape.tech/graphql-armor',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
rule: 'GRAPHQL_NO_COST_ANALYSIS',
|
|
133
|
+
title: 'GraphQL: No Query Cost Analysis',
|
|
134
|
+
regex: /(?:typeDefs|schema)\s*[:=].*(?:Query|Mutation)\s*\{/g,
|
|
135
|
+
severity: 'low',
|
|
136
|
+
cwe: 'CWE-400',
|
|
137
|
+
confidence: 'low',
|
|
138
|
+
description: 'GraphQL schema without query cost analysis. Complex queries can cause DoS.',
|
|
139
|
+
fix: 'Add query complexity analysis: graphql-query-complexity or graphql-armor',
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// ── API Versioning & Documentation ─────────────────────────────────────────
|
|
143
|
+
{
|
|
144
|
+
rule: 'API_DEBUG_ENDPOINT',
|
|
145
|
+
title: 'API: Debug/Test Endpoint in Code',
|
|
146
|
+
regex: /(?:app|router)\.(?:get|post|all)\s*\(\s*['"]\/(?:debug|test|admin|internal|_internal|healthcheck\/debug)/gi,
|
|
147
|
+
severity: 'high',
|
|
148
|
+
cwe: 'CWE-489',
|
|
149
|
+
owasp: 'A05:2021',
|
|
150
|
+
description: 'Debug/test/admin endpoint detected. Ensure it is not accessible in production.',
|
|
151
|
+
fix: 'Remove debug endpoints or protect with auth + environment check',
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// ── Response Headers ───────────────────────────────────────────────────────
|
|
155
|
+
{
|
|
156
|
+
rule: 'API_NO_SECURITY_HEADERS',
|
|
157
|
+
title: 'API: Missing Security Headers (Helmet)',
|
|
158
|
+
regex: /app\.(?:use|listen)\s*\(/g,
|
|
159
|
+
severity: 'low',
|
|
160
|
+
cwe: 'CWE-693',
|
|
161
|
+
owasp: 'A05:2021',
|
|
162
|
+
confidence: 'low',
|
|
163
|
+
description: 'Express app without helmet middleware. Missing security headers (CSP, HSTS, etc.).',
|
|
164
|
+
fix: 'Add helmet: app.use(helmet()) for automatic security headers',
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
// ── Sensitive Data in URL ──────────────────────────────────────────────────
|
|
168
|
+
{
|
|
169
|
+
rule: 'API_SECRET_IN_URL',
|
|
170
|
+
title: 'API: Sensitive Data in URL Parameters',
|
|
171
|
+
regex: /(?:app|router)\.(?:get|post)\s*\(\s*['"][^'"]*(?::token|:apiKey|:password|:secret|:key)\b/g,
|
|
172
|
+
severity: 'high',
|
|
173
|
+
cwe: 'CWE-598',
|
|
174
|
+
owasp: 'A04:2021',
|
|
175
|
+
description: 'Sensitive data in URL parameters gets logged in server logs, browser history, and proxies.',
|
|
176
|
+
fix: 'Move sensitive data to request headers or body',
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// ── Server Configuration ───────────────────────────────────────────────────
|
|
180
|
+
{
|
|
181
|
+
rule: 'API_TRUST_PROXY',
|
|
182
|
+
title: 'API: Trust Proxy Not Configured',
|
|
183
|
+
regex: /app\.set\s*\(\s*['"]trust proxy['"]\s*,\s*true\s*\)/g,
|
|
184
|
+
severity: 'low',
|
|
185
|
+
cwe: 'CWE-346',
|
|
186
|
+
confidence: 'low',
|
|
187
|
+
description: 'trust proxy set to true trusts all proxies. Specify trusted proxy IPs.',
|
|
188
|
+
fix: 'Set specific proxy: app.set("trust proxy", "loopback") or IP address',
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
// ── Denial of Service ──────────────────────────────────────────────────────
|
|
192
|
+
{
|
|
193
|
+
rule: 'API_LARGE_BODY_NO_LIMIT',
|
|
194
|
+
title: 'API: No Request Body Size Limit',
|
|
195
|
+
regex: /(?:express\.json|bodyParser\.json)\s*\(\s*\)/g,
|
|
196
|
+
severity: 'medium',
|
|
197
|
+
cwe: 'CWE-400',
|
|
198
|
+
confidence: 'low',
|
|
199
|
+
description: 'No body size limit configured. Large payloads can cause memory exhaustion.',
|
|
200
|
+
fix: 'Set limit: express.json({ limit: "1mb" })',
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
export class APIFuzzer extends BaseAgent {
|
|
205
|
+
constructor() {
|
|
206
|
+
super('APIFuzzer', 'API endpoint security analysis', 'api');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async analyze(context) {
|
|
210
|
+
const { files } = context;
|
|
211
|
+
const codeFiles = files.filter(f => {
|
|
212
|
+
const ext = path.extname(f).toLowerCase();
|
|
213
|
+
return ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.rb', '.go'].includes(ext);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
let findings = [];
|
|
217
|
+
for (const file of codeFiles) {
|
|
218
|
+
findings = findings.concat(this.scanFileWithPatterns(file, PATTERNS));
|
|
219
|
+
}
|
|
220
|
+
return findings;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export default APIFuzzer;
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthBypassAgent
|
|
3
|
+
* ================
|
|
4
|
+
*
|
|
5
|
+
* Detects authentication and authorization vulnerabilities:
|
|
6
|
+
* JWT misconfig, missing auth middleware, CSRF, session flaws,
|
|
7
|
+
* OAuth misconfig, cookie security, broken access control.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { BaseAgent, createFinding } from './base-agent.js';
|
|
12
|
+
|
|
13
|
+
const PATTERNS = [
|
|
14
|
+
// ── JWT Issues ─────────────────────────────────────────────────────────────
|
|
15
|
+
{
|
|
16
|
+
rule: 'JWT_ALG_NONE',
|
|
17
|
+
title: 'JWT Algorithm None Attack',
|
|
18
|
+
regex: /algorithms?\s*:\s*\[?\s*['"]none['"]/gi,
|
|
19
|
+
severity: 'critical',
|
|
20
|
+
cwe: 'CWE-327',
|
|
21
|
+
owasp: 'A02:2021',
|
|
22
|
+
description: 'Accepting alg: "none" in JWT allows forging tokens without a signature.',
|
|
23
|
+
fix: 'Explicitly set algorithms: ["RS256"] or ["ES256"]. Never accept "none".',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
rule: 'JWT_WEAK_SECRET',
|
|
27
|
+
title: 'JWT Weak HMAC Secret',
|
|
28
|
+
regex: /jwt\.sign\s*\([^)]*,\s*['"][^'"]{1,15}['"]/g,
|
|
29
|
+
severity: 'high',
|
|
30
|
+
cwe: 'CWE-326',
|
|
31
|
+
owasp: 'A02:2021',
|
|
32
|
+
description: 'Short JWT secret (<16 chars) is vulnerable to brute-force. Use a strong random secret.',
|
|
33
|
+
fix: 'Use a 256-bit (32+ char) random secret: require("crypto").randomBytes(32).toString("hex")',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
rule: 'JWT_NO_EXPIRY',
|
|
37
|
+
title: 'JWT Without Expiration',
|
|
38
|
+
regex: /jwt\.sign\s*\([^)]*(?!expiresIn|exp)[^)]*\)/g,
|
|
39
|
+
severity: 'medium',
|
|
40
|
+
cwe: 'CWE-613',
|
|
41
|
+
owasp: 'A07:2021',
|
|
42
|
+
confidence: 'medium',
|
|
43
|
+
description: 'JWTs without expiration never expire. Set a short expiresIn (15m-1h).',
|
|
44
|
+
fix: 'Add { expiresIn: "15m" } to jwt.sign() options',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
rule: 'JWT_VERIFY_DISABLED',
|
|
48
|
+
title: 'JWT Verification Disabled',
|
|
49
|
+
regex: /jwt\.decode\s*\(/g,
|
|
50
|
+
severity: 'high',
|
|
51
|
+
cwe: 'CWE-345',
|
|
52
|
+
owasp: 'A07:2021',
|
|
53
|
+
confidence: 'medium',
|
|
54
|
+
description: 'jwt.decode() does not verify the signature. Use jwt.verify() for authentication.',
|
|
55
|
+
fix: 'Use jwt.verify(token, secret) instead of jwt.decode(token)',
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// ── Cookie Security ────────────────────────────────────────────────────────
|
|
59
|
+
{
|
|
60
|
+
rule: 'COOKIE_NO_HTTPONLY',
|
|
61
|
+
title: 'Cookie Missing httpOnly Flag',
|
|
62
|
+
regex: /(?:cookie|Cookie|setCookie|set-cookie)[^;]*(?!httpOnly|httponly).*(?:secure|domain|path|maxAge|max-age)/gi,
|
|
63
|
+
severity: 'medium',
|
|
64
|
+
cwe: 'CWE-1004',
|
|
65
|
+
owasp: 'A05:2021',
|
|
66
|
+
confidence: 'medium',
|
|
67
|
+
description: 'Cookies without httpOnly can be stolen via XSS. Set httpOnly: true.',
|
|
68
|
+
fix: 'Add httpOnly: true to cookie options',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
rule: 'COOKIE_NO_SECURE',
|
|
72
|
+
title: 'Cookie Missing secure Flag',
|
|
73
|
+
regex: /(?:res\.cookie|setCookie)\s*\([^)]*(?:httpOnly|domain)[^)]*(?!secure)/gi,
|
|
74
|
+
severity: 'medium',
|
|
75
|
+
cwe: 'CWE-614',
|
|
76
|
+
owasp: 'A05:2021',
|
|
77
|
+
confidence: 'medium',
|
|
78
|
+
description: 'Cookies without secure flag are sent over HTTP. Set secure: true in production.',
|
|
79
|
+
fix: 'Add secure: true to cookie options (ensures HTTPS-only transmission)',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
rule: 'COOKIE_SAMESITE_NONE',
|
|
83
|
+
title: 'Cookie SameSite=None Without Secure',
|
|
84
|
+
regex: /sameSite\s*:\s*['"]?none['"]?/gi,
|
|
85
|
+
severity: 'high',
|
|
86
|
+
cwe: 'CWE-1275',
|
|
87
|
+
owasp: 'A05:2021',
|
|
88
|
+
description: 'SameSite=None without Secure exposes cookies to CSRF. Set Secure with SameSite=None.',
|
|
89
|
+
fix: 'Use sameSite: "strict" or "lax". If None is required, also set secure: true.',
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// ── Session Security ───────────────────────────────────────────────────────
|
|
93
|
+
{
|
|
94
|
+
rule: 'SESSION_INSECURE_SECRET',
|
|
95
|
+
title: 'Hardcoded Session Secret',
|
|
96
|
+
regex: /session\s*\(\s*\{[^}]*secret\s*:\s*['"][^'"]{1,20}['"]/g,
|
|
97
|
+
severity: 'high',
|
|
98
|
+
cwe: 'CWE-798',
|
|
99
|
+
owasp: 'A07:2021',
|
|
100
|
+
description: 'Hardcoded short session secret is guessable. Use a strong random value from env.',
|
|
101
|
+
fix: 'Use process.env.SESSION_SECRET with a 256-bit random value',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
rule: 'SESSION_NO_REGENERATE',
|
|
105
|
+
title: 'Session Not Regenerated After Login',
|
|
106
|
+
regex: /(?:login|authenticate|signIn)\s*(?:=|:)\s*(?:async\s+)?(?:function|\([^)]*\)\s*=>)[^}]{50,500}(?!regenerate|destroy|rotate)/g,
|
|
107
|
+
severity: 'medium',
|
|
108
|
+
cwe: 'CWE-384',
|
|
109
|
+
owasp: 'A07:2021',
|
|
110
|
+
confidence: 'low',
|
|
111
|
+
description: 'Sessions should be regenerated after login to prevent session fixation.',
|
|
112
|
+
fix: 'Call req.session.regenerate() after successful authentication',
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
// ── CSRF ───────────────────────────────────────────────────────────────────
|
|
116
|
+
{
|
|
117
|
+
rule: 'CSRF_DISABLED',
|
|
118
|
+
title: 'CSRF Protection Disabled',
|
|
119
|
+
regex: /csrf\s*(?::\s*false|=\s*false|\.disable\(\))/gi,
|
|
120
|
+
severity: 'high',
|
|
121
|
+
cwe: 'CWE-352',
|
|
122
|
+
owasp: 'A01:2021',
|
|
123
|
+
description: 'CSRF protection is explicitly disabled. State-changing requests need CSRF tokens.',
|
|
124
|
+
fix: 'Enable CSRF protection or use SameSite=Strict cookies',
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
// ── OAuth / OIDC ───────────────────────────────────────────────────────────
|
|
128
|
+
{
|
|
129
|
+
rule: 'OAUTH_NO_STATE',
|
|
130
|
+
title: 'OAuth Missing State Parameter',
|
|
131
|
+
regex: /authorize_url|authorization_endpoint|auth_uri.*(?!state)/g,
|
|
132
|
+
severity: 'high',
|
|
133
|
+
cwe: 'CWE-352',
|
|
134
|
+
owasp: 'A07:2021',
|
|
135
|
+
confidence: 'low',
|
|
136
|
+
description: 'OAuth without state parameter is vulnerable to CSRF. Include a random state value.',
|
|
137
|
+
fix: 'Generate a random state parameter and verify it in the callback',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
rule: 'OAUTH_WILDCARD_REDIRECT',
|
|
141
|
+
title: 'OAuth Permissive Redirect URI',
|
|
142
|
+
regex: /redirect_uri\s*[:=]\s*['"]?\*/g,
|
|
143
|
+
severity: 'critical',
|
|
144
|
+
cwe: 'CWE-601',
|
|
145
|
+
owasp: 'A07:2021',
|
|
146
|
+
description: 'Wildcard redirect URI allows OAuth token theft via open redirect.',
|
|
147
|
+
fix: 'Use exact redirect URIs. Never use wildcards in OAuth redirect configuration.',
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
// ── Password Security ──────────────────────────────────────────────────────
|
|
151
|
+
{
|
|
152
|
+
rule: 'WEAK_PASSWORD_HASH',
|
|
153
|
+
title: 'Weak Password Hashing (MD5/SHA)',
|
|
154
|
+
regex: /(?:createHash|hashlib\.)\s*\(\s*['"](?:md5|sha1|sha256)['"]\s*\).*(?:password|passwd)/gi,
|
|
155
|
+
severity: 'critical',
|
|
156
|
+
cwe: 'CWE-916',
|
|
157
|
+
owasp: 'A02:2021',
|
|
158
|
+
description: 'MD5/SHA are not suitable for password hashing. Use bcrypt, scrypt, or argon2.',
|
|
159
|
+
fix: 'Use bcrypt.hash(password, 12) or argon2.hash(password)',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
rule: 'PLAINTEXT_PASSWORD_COMPARISON',
|
|
163
|
+
title: 'Plaintext Password Comparison',
|
|
164
|
+
regex: /(?:password|passwd)\s*(?:===?|==)\s*(?:req\.|request\.|body\.|query\.)/g,
|
|
165
|
+
severity: 'critical',
|
|
166
|
+
cwe: 'CWE-256',
|
|
167
|
+
owasp: 'A02:2021',
|
|
168
|
+
description: 'Comparing passwords in plaintext means they are stored unhashed. Hash passwords.',
|
|
169
|
+
fix: 'Use bcrypt.compare(inputPassword, hashedPassword)',
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// ── Missing Auth Checks ────────────────────────────────────────────────────
|
|
173
|
+
{
|
|
174
|
+
rule: 'BOLA_DIRECT_ID',
|
|
175
|
+
title: 'Broken Object-Level Authorization (BOLA)',
|
|
176
|
+
regex: /(?:findById|findOne|findUnique|findByPk)\s*\(\s*(?:req\.params|req\.query|ctx\.params)/g,
|
|
177
|
+
severity: 'high',
|
|
178
|
+
cwe: 'CWE-639',
|
|
179
|
+
owasp: 'A01:2021',
|
|
180
|
+
description: 'Fetching by user-supplied ID without ownership check enables BOLA/IDOR.',
|
|
181
|
+
fix: 'Add ownership check: .findFirst({ where: { id: params.id, userId: session.user.id } })',
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
rule: 'MASS_ASSIGNMENT',
|
|
185
|
+
title: 'Mass Assignment Vulnerability',
|
|
186
|
+
regex: /(?:\.create|\.update|\.insert)\s*\(\s*(?:req\.body|request\.body|ctx\.request\.body)/g,
|
|
187
|
+
severity: 'high',
|
|
188
|
+
cwe: 'CWE-915',
|
|
189
|
+
owasp: 'A01:2021',
|
|
190
|
+
description: 'Passing full request body to create/update enables mass assignment attacks.',
|
|
191
|
+
fix: 'Destructure only allowed fields: const { name, email } = req.body; Model.create({ name, email })',
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
// ── Timing Attacks ─────────────────────────────────────────────────────────
|
|
195
|
+
{
|
|
196
|
+
rule: 'TIMING_ATTACK_COMPARISON',
|
|
197
|
+
title: 'Timing Attack: String Comparison',
|
|
198
|
+
regex: /(?:apiKey|api_key|token|secret|signature|hmac)\s*(?:===?|!==?)\s*/gi,
|
|
199
|
+
severity: 'medium',
|
|
200
|
+
cwe: 'CWE-208',
|
|
201
|
+
owasp: 'A02:2021',
|
|
202
|
+
confidence: 'medium',
|
|
203
|
+
description: 'Direct string comparison of secrets is vulnerable to timing attacks.',
|
|
204
|
+
fix: 'Use crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b))',
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
// ── Rate Limiting ──────────────────────────────────────────────────────────
|
|
208
|
+
{
|
|
209
|
+
rule: 'NO_RATE_LIMIT_LOGIN',
|
|
210
|
+
title: 'No Rate Limiting on Authentication',
|
|
211
|
+
regex: /(?:\/login|\/signin|\/auth|\/register|\/signup|\/reset-password)\s*['"]/g,
|
|
212
|
+
severity: 'medium',
|
|
213
|
+
cwe: 'CWE-307',
|
|
214
|
+
owasp: 'A07:2021',
|
|
215
|
+
confidence: 'low',
|
|
216
|
+
description: 'Auth endpoints without rate limiting are vulnerable to brute-force attacks.',
|
|
217
|
+
fix: 'Add rate limiting: express-rate-limit, @upstash/ratelimit, or Cloudflare rules',
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
// ── Weak Crypto ────────────────────────────────────────────────────────────
|
|
221
|
+
{
|
|
222
|
+
rule: 'WEAK_CRYPTO_MD5',
|
|
223
|
+
title: 'Weak Cryptography: MD5',
|
|
224
|
+
regex: /createHash\s*\(\s*['"]md5['"]\s*\)/gi,
|
|
225
|
+
severity: 'medium',
|
|
226
|
+
cwe: 'CWE-328',
|
|
227
|
+
owasp: 'A02:2021',
|
|
228
|
+
description: 'MD5 is cryptographically broken. Use SHA-256 or SHA-3 for integrity checks.',
|
|
229
|
+
fix: 'Use createHash("sha256") instead of createHash("md5")',
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
rule: 'WEAK_CRYPTO_SHA1',
|
|
233
|
+
title: 'Weak Cryptography: SHA-1',
|
|
234
|
+
regex: /createHash\s*\(\s*['"]sha1['"]\s*\)/gi,
|
|
235
|
+
severity: 'medium',
|
|
236
|
+
cwe: 'CWE-328',
|
|
237
|
+
owasp: 'A02:2021',
|
|
238
|
+
description: 'SHA-1 is collision-prone. Use SHA-256 or SHA-3.',
|
|
239
|
+
fix: 'Use createHash("sha256") instead of createHash("sha1")',
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
rule: 'WEAK_CRYPTO_ECB',
|
|
243
|
+
title: 'Weak Cryptography: ECB Mode',
|
|
244
|
+
regex: /(?:AES|DES).*ECB|createCipheriv\s*\(\s*['"](?:aes-\d+-ecb|des-ecb)['"]/gi,
|
|
245
|
+
severity: 'high',
|
|
246
|
+
cwe: 'CWE-327',
|
|
247
|
+
owasp: 'A02:2021',
|
|
248
|
+
description: 'ECB mode leaks patterns in ciphertext. Use CBC or GCM mode.',
|
|
249
|
+
fix: 'Use AES-256-GCM: createCipheriv("aes-256-gcm", key, iv)',
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
rule: 'HARDCODED_CRYPTO_KEY',
|
|
253
|
+
title: 'Hardcoded Encryption Key',
|
|
254
|
+
regex: /createCipher(?:iv)?\s*\([^,]+,\s*['"][^'"]+['"]/g,
|
|
255
|
+
severity: 'high',
|
|
256
|
+
cwe: 'CWE-321',
|
|
257
|
+
owasp: 'A02:2021',
|
|
258
|
+
description: 'Hardcoded encryption key in source code. Load from environment variables.',
|
|
259
|
+
fix: 'Use process.env.ENCRYPTION_KEY instead of hardcoded string',
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
rule: 'WEAK_RANDOM',
|
|
263
|
+
title: 'Insecure Random Number Generator',
|
|
264
|
+
regex: /Math\.random\s*\(\s*\).*(?:token|secret|key|password|salt|nonce|csrf|session)/gi,
|
|
265
|
+
severity: 'high',
|
|
266
|
+
cwe: 'CWE-338',
|
|
267
|
+
owasp: 'A02:2021',
|
|
268
|
+
description: 'Math.random() is not cryptographically secure. Use crypto.randomBytes().',
|
|
269
|
+
fix: 'Use crypto.randomBytes(32).toString("hex") or crypto.randomUUID()',
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
// ── TLS/SSL ────────────────────────────────────────────────────────────────
|
|
273
|
+
{
|
|
274
|
+
rule: 'TLS_REJECT_UNAUTHORIZED',
|
|
275
|
+
title: 'TLS Certificate Verification Disabled',
|
|
276
|
+
regex: /NODE_TLS_REJECT_UNAUTHORIZED\s*[=:]\s*['"]?0['"]?/g,
|
|
277
|
+
severity: 'critical',
|
|
278
|
+
cwe: 'CWE-295',
|
|
279
|
+
owasp: 'A02:2021',
|
|
280
|
+
description: 'Disabling TLS verification exposes app to MITM attacks.',
|
|
281
|
+
fix: 'Remove NODE_TLS_REJECT_UNAUTHORIZED=0. Use proper CA certificates.',
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
rule: 'TLS_REJECT_UNAUTH_FALSE',
|
|
285
|
+
title: 'TLS rejectUnauthorized: false',
|
|
286
|
+
regex: /\brejectUnauthorized\s*:\s*false\b/g,
|
|
287
|
+
severity: 'high',
|
|
288
|
+
cwe: 'CWE-295',
|
|
289
|
+
owasp: 'A02:2021',
|
|
290
|
+
description: 'rejectUnauthorized: false disables TLS certificate checking.',
|
|
291
|
+
fix: 'Remove rejectUnauthorized: false, or use a proper CA bundle',
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
rule: 'TLS_VERIFY_FALSE_PYTHON',
|
|
295
|
+
title: 'TLS verify=False (Python)',
|
|
296
|
+
regex: /\brequests\.\w+\s*\([^)]*\bverify\s*=\s*False\b/g,
|
|
297
|
+
severity: 'high',
|
|
298
|
+
cwe: 'CWE-295',
|
|
299
|
+
owasp: 'A02:2021',
|
|
300
|
+
description: 'Python requests with verify=False disables SSL cert verification.',
|
|
301
|
+
fix: 'Remove verify=False, or pass verify="/path/to/ca-bundle.crt"',
|
|
302
|
+
},
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
export class AuthBypassAgent extends BaseAgent {
|
|
306
|
+
constructor() {
|
|
307
|
+
super('AuthBypassAgent', 'Detect authentication and authorization vulnerabilities', 'auth');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async analyze(context) {
|
|
311
|
+
const { files } = context;
|
|
312
|
+
const codeFiles = files.filter(f => {
|
|
313
|
+
const ext = path.extname(f).toLowerCase();
|
|
314
|
+
return ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.rb', '.php', '.go'].includes(ext);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
let findings = [];
|
|
318
|
+
for (const file of codeFiles) {
|
|
319
|
+
findings = findings.concat(this.scanFileWithPatterns(file, PATTERNS));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return findings;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export default AuthBypassAgent;
|