ship-safe 4.0.0 → 4.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.
@@ -1,326 +1,348 @@
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;
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)[^;]{0,100}(?:secure|domain|path|maxAge|max-age)(?![^;]*httpOnly)/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*=>)/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
+ // ── Django/Flask Security ────────────────────────────────────────────────
273
+ {
274
+ rule: 'DJANGO_DEBUG_TRUE',
275
+ title: 'Django: DEBUG = True',
276
+ regex: /\bDEBUG\s*=\s*True\b/g,
277
+ severity: 'high',
278
+ cwe: 'CWE-215',
279
+ owasp: 'A05:2021',
280
+ description: 'Django DEBUG mode exposes stack traces, SQL queries, and settings to users.',
281
+ fix: 'Set DEBUG = False in production. Use environment variable: DEBUG = os.getenv("DEBUG", "False") == "True"',
282
+ },
283
+ {
284
+ rule: 'FLASK_SECRET_KEY_HARDCODED',
285
+ title: 'Flask: Hardcoded Secret Key',
286
+ regex: /app\.secret_key\s*=\s*['"][^'"]{1,30}['"]/g,
287
+ severity: 'high',
288
+ cwe: 'CWE-798',
289
+ owasp: 'A07:2021',
290
+ description: 'Flask secret key is hardcoded. Session cookies can be forged.',
291
+ fix: 'Use os.environ.get("SECRET_KEY") with a 256-bit random value',
292
+ },
293
+
294
+ // ── TLS/SSL ────────────────────────────────────────────────────────────────
295
+ {
296
+ rule: 'TLS_REJECT_UNAUTHORIZED',
297
+ title: 'TLS Certificate Verification Disabled',
298
+ regex: /NODE_TLS_REJECT_UNAUTHORIZED\s*[=:]\s*['"]?0['"]?/g,
299
+ severity: 'critical',
300
+ cwe: 'CWE-295',
301
+ owasp: 'A02:2021',
302
+ description: 'Disabling TLS verification exposes app to MITM attacks.',
303
+ fix: 'Remove NODE_TLS_REJECT_UNAUTHORIZED=0. Use proper CA certificates.',
304
+ },
305
+ {
306
+ rule: 'TLS_REJECT_UNAUTH_FALSE',
307
+ title: 'TLS rejectUnauthorized: false',
308
+ regex: /\brejectUnauthorized\s*:\s*false\b/g,
309
+ severity: 'high',
310
+ cwe: 'CWE-295',
311
+ owasp: 'A02:2021',
312
+ description: 'rejectUnauthorized: false disables TLS certificate checking.',
313
+ fix: 'Remove rejectUnauthorized: false, or use a proper CA bundle',
314
+ },
315
+ {
316
+ rule: 'TLS_VERIFY_FALSE_PYTHON',
317
+ title: 'TLS verify=False (Python)',
318
+ regex: /\brequests\.\w+\s*\([^)]*\bverify\s*=\s*False\b/g,
319
+ severity: 'high',
320
+ cwe: 'CWE-295',
321
+ owasp: 'A02:2021',
322
+ description: 'Python requests with verify=False disables SSL cert verification.',
323
+ fix: 'Remove verify=False, or pass verify="/path/to/ca-bundle.crt"',
324
+ },
325
+ ];
326
+
327
+ export class AuthBypassAgent extends BaseAgent {
328
+ constructor() {
329
+ super('AuthBypassAgent', 'Detect authentication and authorization vulnerabilities', 'auth');
330
+ }
331
+
332
+ async analyze(context) {
333
+ const { files } = context;
334
+ const codeFiles = files.filter(f => {
335
+ const ext = path.extname(f).toLowerCase();
336
+ return ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.rb', '.php', '.go'].includes(ext);
337
+ });
338
+
339
+ let findings = [];
340
+ for (const file of codeFiles) {
341
+ findings = findings.concat(this.scanFileWithPatterns(file, PATTERNS));
342
+ }
343
+
344
+ return findings;
345
+ }
346
+ }
347
+
348
+ export default AuthBypassAgent;