jaku.sh 1.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/LICENSE +52 -0
- package/README.md +636 -0
- package/action.yml +264 -0
- package/bin/jaku +2 -0
- package/package.json +62 -0
- package/src/agents/ai-agent.js +175 -0
- package/src/agents/api-agent.js +95 -0
- package/src/agents/base-agent.js +158 -0
- package/src/agents/crawl-agent.js +175 -0
- package/src/agents/event-bus.js +59 -0
- package/src/agents/findings-ledger.js +410 -0
- package/src/agents/logic-agent.js +144 -0
- package/src/agents/orchestrator.js +323 -0
- package/src/agents/qa-agent.js +149 -0
- package/src/agents/security-agent.js +211 -0
- package/src/cli.js +423 -0
- package/src/core/accessibility-checker.js +171 -0
- package/src/core/ai/ai-endpoint-detector.js +227 -0
- package/src/core/ai/guardrail-prober.js +362 -0
- package/src/core/ai/indirect-injector.js +106 -0
- package/src/core/ai/jailbreak-tester.js +212 -0
- package/src/core/ai/model-dos-tester.js +174 -0
- package/src/core/ai/model-fingerprinter.js +246 -0
- package/src/core/ai/multi-turn-attacker.js +297 -0
- package/src/core/ai/output-analyzer.js +182 -0
- package/src/core/ai/prompt-injector.js +543 -0
- package/src/core/ai/system-prompt-extractor.js +244 -0
- package/src/core/api/api-key-auditor.js +266 -0
- package/src/core/api/auth-flow-tester.js +430 -0
- package/src/core/api/cors-ws-tester.js +263 -0
- package/src/core/api/graphql-tester.js +287 -0
- package/src/core/api/oauth-prober.js +343 -0
- package/src/core/auth-manager.js +902 -0
- package/src/core/broken-flow-detector.js +207 -0
- package/src/core/browser-manager.js +119 -0
- package/src/core/console-monitor.js +111 -0
- package/src/core/crawler.js +430 -0
- package/src/core/csr-waiter.js +410 -0
- package/src/core/form-validator.js +240 -0
- package/src/core/logic/abuse-pattern-scanner.js +291 -0
- package/src/core/logic/access-boundary-tester.js +448 -0
- package/src/core/logic/business-rule-inferrer.js +196 -0
- package/src/core/logic/graphql-auditor.js +298 -0
- package/src/core/logic/parameter-polluter.js +212 -0
- package/src/core/logic/pricing-exploiter.js +299 -0
- package/src/core/logic/race-condition-detector.js +222 -0
- package/src/core/logic/workflow-enforcer.js +284 -0
- package/src/core/performance-checker.js +204 -0
- package/src/core/responsive-checker.js +228 -0
- package/src/core/security/cors-prober.js +150 -0
- package/src/core/security/csrf-prober.js +217 -0
- package/src/core/security/dependency-auditor.js +182 -0
- package/src/core/security/file-upload-tester.js +340 -0
- package/src/core/security/header-analyzer.js +324 -0
- package/src/core/security/infra-scanner.js +391 -0
- package/src/core/security/path-traversal.js +112 -0
- package/src/core/security/prototype-pollution.js +147 -0
- package/src/core/security/secret-detector.js +517 -0
- package/src/core/security/sqli-prober.js +257 -0
- package/src/core/security/tls-checker.js +223 -0
- package/src/core/security/xss-scanner.js +225 -0
- package/src/core/test-generator.js +339 -0
- package/src/core/test-runner.js +398 -0
- package/src/reporting/diff-reporter.js +172 -0
- package/src/reporting/report-generator.js +408 -0
- package/src/reporting/sarif-generator.js +190 -0
- package/src/utils/config.js +57 -0
- package/src/utils/finding.js +67 -0
- package/src/utils/logger.js +50 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { createFinding } from '../../utils/finding.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AuthFlowTester — Tests authentication and session management security.
|
|
5
|
+
*
|
|
6
|
+
* Probes:
|
|
7
|
+
* - JWT alg:none attack (strip signature)
|
|
8
|
+
* - JWT weak secret (common signing keys)
|
|
9
|
+
* - JWT expiry issues (no exp, unreasonably long)
|
|
10
|
+
* - Session fixation (ID unchanged after login)
|
|
11
|
+
* - Password policy (min length, common passwords)
|
|
12
|
+
* - Password reset flow security
|
|
13
|
+
* - MFA bypass (response manipulation, OTP reuse)
|
|
14
|
+
*/
|
|
15
|
+
export class AuthFlowTester {
|
|
16
|
+
constructor(logger) {
|
|
17
|
+
this.logger = logger;
|
|
18
|
+
|
|
19
|
+
this.COMMON_JWT_SECRETS = [
|
|
20
|
+
'secret', 'password', '123456', 'admin', 'key', 'jwt_secret',
|
|
21
|
+
'supersecret', 'changeme', 'test', 'default', 'mysecret',
|
|
22
|
+
'jwt', 'token', 'your-256-bit-secret', 'HS256-secret',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
this.COMMON_PASSWORDS = [
|
|
26
|
+
'123456', 'password', '12345678', 'qwerty', 'abc123',
|
|
27
|
+
'password1', 'admin', 'letmein', 'welcome', 'monkey',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
this.AUTH_ENDPOINTS = {
|
|
31
|
+
login: ['/login', '/signin', '/auth/login', '/api/auth/login', '/api/login', '/api/signin', '/api/v1/auth/login'],
|
|
32
|
+
register: ['/register', '/signup', '/auth/register', '/api/auth/register', '/api/register', '/api/signup'],
|
|
33
|
+
reset: ['/forgot-password', '/reset-password', '/api/auth/forgot', '/api/auth/reset', '/api/password/reset'],
|
|
34
|
+
mfa: ['/verify-otp', '/mfa/verify', '/2fa/verify', '/api/auth/mfa', '/api/auth/2fa', '/api/verify-otp'],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Test authentication flows.
|
|
40
|
+
*/
|
|
41
|
+
async test(surfaceInventory) {
|
|
42
|
+
const findings = [];
|
|
43
|
+
const baseUrl = this._getBaseUrl(surfaceInventory);
|
|
44
|
+
if (!baseUrl) return findings;
|
|
45
|
+
|
|
46
|
+
this.logger?.info?.('Auth Flow Tester: starting tests');
|
|
47
|
+
|
|
48
|
+
// 1. JWT analysis
|
|
49
|
+
const jwtFindings = await this._testJWT(surfaceInventory);
|
|
50
|
+
findings.push(...jwtFindings);
|
|
51
|
+
|
|
52
|
+
// 2. Password policy
|
|
53
|
+
const passwordFindings = await this._testPasswordPolicy(baseUrl);
|
|
54
|
+
findings.push(...passwordFindings);
|
|
55
|
+
|
|
56
|
+
// 3. Password reset flow
|
|
57
|
+
const resetFindings = await this._testPasswordReset(baseUrl);
|
|
58
|
+
findings.push(...resetFindings);
|
|
59
|
+
|
|
60
|
+
// 4. MFA bypass
|
|
61
|
+
const mfaFindings = await this._testMFABypass(baseUrl);
|
|
62
|
+
findings.push(...mfaFindings);
|
|
63
|
+
|
|
64
|
+
// 5. Session management
|
|
65
|
+
const sessionFindings = await this._testSessionManagement(baseUrl);
|
|
66
|
+
findings.push(...sessionFindings);
|
|
67
|
+
|
|
68
|
+
this.logger?.info?.(`Auth Flow Tester: found ${findings.length} issues`);
|
|
69
|
+
return findings;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Test JWT security — alg:none, weak secret, expiry.
|
|
74
|
+
*/
|
|
75
|
+
async _testJWT(surfaceInventory) {
|
|
76
|
+
const findings = [];
|
|
77
|
+
const baseUrl = this._getBaseUrl(surfaceInventory);
|
|
78
|
+
if (!baseUrl) return findings;
|
|
79
|
+
|
|
80
|
+
// Try to get a JWT by hitting login endpoints
|
|
81
|
+
for (const path of this.AUTH_ENDPOINTS.login) {
|
|
82
|
+
try {
|
|
83
|
+
const url = new URL(path, baseUrl).href;
|
|
84
|
+
const controller = new AbortController();
|
|
85
|
+
const timeout = setTimeout(() => controller.abort(), 8000);
|
|
86
|
+
|
|
87
|
+
const response = await fetch(url, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: { 'Content-Type': 'application/json' },
|
|
90
|
+
body: JSON.stringify({ email: 'test@test.com', password: 'test123' }),
|
|
91
|
+
signal: controller.signal,
|
|
92
|
+
});
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
|
|
95
|
+
const text = await response.text();
|
|
96
|
+
|
|
97
|
+
// Look for JWT patterns in response
|
|
98
|
+
const jwtMatch = text.match(/eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/);
|
|
99
|
+
if (jwtMatch) {
|
|
100
|
+
const jwt = jwtMatch[0];
|
|
101
|
+
const jwtFindings = this._analyzeJWT(jwt, url);
|
|
102
|
+
findings.push(...jwtFindings);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Also check response headers for JWT
|
|
106
|
+
const authHeader = response.headers.get('authorization') || '';
|
|
107
|
+
const setCookie = response.headers.get('set-cookie') || '';
|
|
108
|
+
for (const header of [authHeader, setCookie]) {
|
|
109
|
+
const headerJwt = header.match(/eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/);
|
|
110
|
+
if (headerJwt) {
|
|
111
|
+
const jwtFindings = this._analyzeJWT(headerJwt[0], url);
|
|
112
|
+
findings.push(...jwtFindings);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return findings;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Analyze a JWT for security issues.
|
|
125
|
+
*/
|
|
126
|
+
_analyzeJWT(jwt, sourceUrl) {
|
|
127
|
+
const findings = [];
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const parts = jwt.split('.');
|
|
131
|
+
if (parts.length < 2) return findings;
|
|
132
|
+
|
|
133
|
+
// Decode header and payload
|
|
134
|
+
const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString());
|
|
135
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
136
|
+
|
|
137
|
+
// Check alg:none
|
|
138
|
+
if (!header.alg || header.alg.toLowerCase() === 'none') {
|
|
139
|
+
findings.push(createFinding({
|
|
140
|
+
module: 'api',
|
|
141
|
+
title: 'JWT Algorithm None: Signature Bypass',
|
|
142
|
+
severity: 'critical',
|
|
143
|
+
affected_surface: sourceUrl,
|
|
144
|
+
description: `JWT uses alg:none, meaning the signature is not verified. An attacker can forge any JWT payload without knowing the signing key.`,
|
|
145
|
+
reproduction: [
|
|
146
|
+
`1. Intercept the JWT from ${sourceUrl}`,
|
|
147
|
+
`2. Modify the header to {"alg":"none"}`,
|
|
148
|
+
`3. Change the payload (e.g., set admin:true)`,
|
|
149
|
+
`4. Strip the signature and use the forged token`,
|
|
150
|
+
],
|
|
151
|
+
evidence: `JWT header: ${JSON.stringify(header)}`,
|
|
152
|
+
remediation: 'Explicitly validate the JWT algorithm server-side. Reject tokens with alg:none. Use an allowlist of accepted algorithms (e.g., HS256 only).',
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check expiry
|
|
157
|
+
if (!payload.exp) {
|
|
158
|
+
findings.push(createFinding({
|
|
159
|
+
module: 'api',
|
|
160
|
+
title: 'JWT Missing Expiry: Permanent Token',
|
|
161
|
+
severity: 'high',
|
|
162
|
+
affected_surface: sourceUrl,
|
|
163
|
+
description: `JWT has no expiry claim (exp). Once issued, this token never expires — a stolen token grants permanent access.`,
|
|
164
|
+
evidence: `JWT payload keys: ${Object.keys(payload).join(', ')} — no "exp" claim`,
|
|
165
|
+
remediation: 'Always include an exp claim in JWTs. Set short-lived tokens (15m-1h) with refresh token rotation.',
|
|
166
|
+
}));
|
|
167
|
+
} else {
|
|
168
|
+
const expDate = new Date(payload.exp * 1000);
|
|
169
|
+
const expiresInDays = (expDate - Date.now()) / (1000 * 60 * 60 * 24);
|
|
170
|
+
if (expiresInDays > 30) {
|
|
171
|
+
findings.push(createFinding({
|
|
172
|
+
module: 'api',
|
|
173
|
+
title: 'JWT Excessive Expiry: Token Lives Too Long',
|
|
174
|
+
severity: 'medium',
|
|
175
|
+
affected_surface: sourceUrl,
|
|
176
|
+
description: `JWT expires in ${Math.round(expiresInDays)} days. Long-lived tokens increase the window for stolen token abuse.`,
|
|
177
|
+
evidence: `exp: ${payload.exp} (${expDate.toISOString()})`,
|
|
178
|
+
remediation: 'Reduce JWT lifetime to 15 minutes–1 hour. Use refresh tokens for session continuity.',
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check for sensitive data in payload
|
|
184
|
+
const sensitiveKeys = ['password', 'secret', 'ssn', 'credit_card', 'cc_number'];
|
|
185
|
+
const foundSensitive = Object.keys(payload).filter(k =>
|
|
186
|
+
sensitiveKeys.some(s => k.toLowerCase().includes(s))
|
|
187
|
+
);
|
|
188
|
+
if (foundSensitive.length > 0) {
|
|
189
|
+
findings.push(createFinding({
|
|
190
|
+
module: 'api',
|
|
191
|
+
title: 'JWT Contains Sensitive Data',
|
|
192
|
+
severity: 'high',
|
|
193
|
+
affected_surface: sourceUrl,
|
|
194
|
+
description: `JWT payload contains potentially sensitive fields: ${foundSensitive.join(', ')}. JWTs are base64-encoded, not encrypted — anyone can decode them.`,
|
|
195
|
+
evidence: `Sensitive fields in payload: ${foundSensitive.join(', ')}`,
|
|
196
|
+
remediation: 'Never store sensitive data in JWTs. JWTs are easily decoded. Store sensitive data server-side and reference by ID.',
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// Invalid JWT structure
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return findings;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Test password policy.
|
|
208
|
+
*/
|
|
209
|
+
async _testPasswordPolicy(baseUrl) {
|
|
210
|
+
const findings = [];
|
|
211
|
+
|
|
212
|
+
for (const path of this.AUTH_ENDPOINTS.register) {
|
|
213
|
+
try {
|
|
214
|
+
const url = new URL(path, baseUrl).href;
|
|
215
|
+
|
|
216
|
+
// Test weak password acceptance
|
|
217
|
+
for (const weakPw of this.COMMON_PASSWORDS.slice(0, 3)) {
|
|
218
|
+
const controller = new AbortController();
|
|
219
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
220
|
+
|
|
221
|
+
const response = await fetch(url, {
|
|
222
|
+
method: 'POST',
|
|
223
|
+
headers: { 'Content-Type': 'application/json' },
|
|
224
|
+
body: JSON.stringify({
|
|
225
|
+
email: `test_${Date.now()}@example.com`,
|
|
226
|
+
password: weakPw,
|
|
227
|
+
username: `test_${Date.now()}`,
|
|
228
|
+
}),
|
|
229
|
+
signal: controller.signal,
|
|
230
|
+
});
|
|
231
|
+
clearTimeout(timeout);
|
|
232
|
+
|
|
233
|
+
if (response.ok) {
|
|
234
|
+
const text = await response.text();
|
|
235
|
+
if (/success|created|registered|welcome/i.test(text) &&
|
|
236
|
+
!/weak|too short|minimum|stronger|invalid.*password/i.test(text)) {
|
|
237
|
+
findings.push(createFinding({
|
|
238
|
+
module: 'api',
|
|
239
|
+
title: 'Weak Password Policy: Common Password Accepted',
|
|
240
|
+
severity: 'high',
|
|
241
|
+
affected_surface: url,
|
|
242
|
+
description: `Registration endpoint accepted the common password "${weakPw}". This allows users to create accounts with passwords that are trivially brute-forceable.`,
|
|
243
|
+
reproduction: [
|
|
244
|
+
`1. POST to ${url} with password: "${weakPw}"`,
|
|
245
|
+
`2. Account is created without password strength error`,
|
|
246
|
+
],
|
|
247
|
+
evidence: `Accepted password: "${weakPw}"`,
|
|
248
|
+
remediation: 'Enforce minimum password length (12+ chars), check against breach databases (Have I Been Pwned API), require complexity (mixed case, numbers, symbols).',
|
|
249
|
+
}));
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} catch {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return findings;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Test password reset flow.
|
|
264
|
+
*/
|
|
265
|
+
async _testPasswordReset(baseUrl) {
|
|
266
|
+
const findings = [];
|
|
267
|
+
|
|
268
|
+
for (const path of this.AUTH_ENDPOINTS.reset) {
|
|
269
|
+
try {
|
|
270
|
+
const url = new URL(path, baseUrl).href;
|
|
271
|
+
const controller = new AbortController();
|
|
272
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
273
|
+
|
|
274
|
+
// Test with arbitrary token
|
|
275
|
+
const response = await fetch(url, {
|
|
276
|
+
method: 'POST',
|
|
277
|
+
headers: { 'Content-Type': 'application/json' },
|
|
278
|
+
body: JSON.stringify({
|
|
279
|
+
token: '000000',
|
|
280
|
+
password: 'NewPassword123!',
|
|
281
|
+
email: 'test@example.com',
|
|
282
|
+
}),
|
|
283
|
+
signal: controller.signal,
|
|
284
|
+
});
|
|
285
|
+
clearTimeout(timeout);
|
|
286
|
+
|
|
287
|
+
if (response.ok) {
|
|
288
|
+
const text = await response.text();
|
|
289
|
+
if (/success|reset|changed|updated/i.test(text) &&
|
|
290
|
+
!/invalid|expired|wrong|not found/i.test(text)) {
|
|
291
|
+
findings.push(createFinding({
|
|
292
|
+
module: 'api',
|
|
293
|
+
title: 'Password Reset: Weak Token Accepted',
|
|
294
|
+
severity: 'critical',
|
|
295
|
+
affected_surface: url,
|
|
296
|
+
description: `Password reset endpoint accepted a trivial token ("000000"). An attacker can reset any user's password by guessing or brute-forcing the token.`,
|
|
297
|
+
reproduction: [
|
|
298
|
+
`1. POST to ${url} with token: "000000"`,
|
|
299
|
+
`2. Password is reset without valid token verificaton`,
|
|
300
|
+
],
|
|
301
|
+
evidence: `Trivial token "000000" accepted`,
|
|
302
|
+
remediation: 'Use cryptographically random tokens (UUID v4 or 32+ byte random). Expire tokens after 15 minutes. Rate limit reset attempts. Invalidate token after single use.',
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} catch {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return findings;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Test MFA bypass.
|
|
316
|
+
*/
|
|
317
|
+
async _testMFABypass(baseUrl) {
|
|
318
|
+
const findings = [];
|
|
319
|
+
|
|
320
|
+
for (const path of this.AUTH_ENDPOINTS.mfa) {
|
|
321
|
+
try {
|
|
322
|
+
const url = new URL(path, baseUrl).href;
|
|
323
|
+
|
|
324
|
+
// Test OTP bypass
|
|
325
|
+
const bypasses = [
|
|
326
|
+
{ code: '000000', description: 'trivial OTP' },
|
|
327
|
+
{ code: '', description: 'empty OTP' },
|
|
328
|
+
{ verified: true, description: 'verified flag' },
|
|
329
|
+
];
|
|
330
|
+
|
|
331
|
+
for (const bypass of bypasses) {
|
|
332
|
+
const controller = new AbortController();
|
|
333
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
334
|
+
|
|
335
|
+
const response = await fetch(url, {
|
|
336
|
+
method: 'POST',
|
|
337
|
+
headers: { 'Content-Type': 'application/json' },
|
|
338
|
+
body: JSON.stringify(bypass),
|
|
339
|
+
signal: controller.signal,
|
|
340
|
+
});
|
|
341
|
+
clearTimeout(timeout);
|
|
342
|
+
|
|
343
|
+
if (response.ok) {
|
|
344
|
+
const text = await response.text();
|
|
345
|
+
if (/success|verified|authenticated|token/i.test(text) &&
|
|
346
|
+
!/invalid|wrong|expired|failed/i.test(text)) {
|
|
347
|
+
findings.push(createFinding({
|
|
348
|
+
module: 'api',
|
|
349
|
+
title: `MFA Bypass: ${bypass.description}`,
|
|
350
|
+
severity: 'critical',
|
|
351
|
+
affected_surface: url,
|
|
352
|
+
description: `MFA verification at ${url} was bypassed using ${bypass.description}. This completely undermines multi-factor authentication.`,
|
|
353
|
+
reproduction: [
|
|
354
|
+
`1. POST to ${url} with ${JSON.stringify(bypass)}`,
|
|
355
|
+
`2. MFA is marked as verified`,
|
|
356
|
+
],
|
|
357
|
+
evidence: `Bypass: ${JSON.stringify(bypass)}`,
|
|
358
|
+
remediation: 'Validate OTP codes server-side against time-based secret. Never accept empty codes or client-supplied "verified" flags. Implement rate limiting (3-5 attempts max). Lock account after excessive failures.',
|
|
359
|
+
}));
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
} catch {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return findings;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Test session management.
|
|
374
|
+
*/
|
|
375
|
+
async _testSessionManagement(baseUrl) {
|
|
376
|
+
const findings = [];
|
|
377
|
+
|
|
378
|
+
// Check if login endpoint returns Set-Cookie with security flags
|
|
379
|
+
for (const path of this.AUTH_ENDPOINTS.login) {
|
|
380
|
+
try {
|
|
381
|
+
const url = new URL(path, baseUrl).href;
|
|
382
|
+
const controller = new AbortController();
|
|
383
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
384
|
+
|
|
385
|
+
const response = await fetch(url, {
|
|
386
|
+
method: 'POST',
|
|
387
|
+
headers: { 'Content-Type': 'application/json' },
|
|
388
|
+
body: JSON.stringify({ email: 'test@test.com', password: 'test' }),
|
|
389
|
+
signal: controller.signal,
|
|
390
|
+
});
|
|
391
|
+
clearTimeout(timeout);
|
|
392
|
+
|
|
393
|
+
const setCookie = response.headers.get('set-cookie') || '';
|
|
394
|
+
if (setCookie) {
|
|
395
|
+
const issues = [];
|
|
396
|
+
if (!/httponly/i.test(setCookie)) issues.push('HttpOnly');
|
|
397
|
+
if (!/secure/i.test(setCookie)) issues.push('Secure');
|
|
398
|
+
if (!/samesite/i.test(setCookie)) issues.push('SameSite');
|
|
399
|
+
|
|
400
|
+
if (issues.length >= 2) {
|
|
401
|
+
findings.push(createFinding({
|
|
402
|
+
module: 'api',
|
|
403
|
+
title: 'Session Cookie Missing Security Flags',
|
|
404
|
+
severity: 'medium',
|
|
405
|
+
affected_surface: url,
|
|
406
|
+
description: `Session cookie is missing: ${issues.join(', ')}. Without HttpOnly, JavaScript can steal cookies. Without Secure, cookies are sent over HTTP. Without SameSite, cookies are vulnerable to CSRF.`,
|
|
407
|
+
evidence: `Set-Cookie: ${setCookie.substring(0, 200)}`,
|
|
408
|
+
remediation: 'Set all session cookies with HttpOnly, Secure, and SameSite=Strict (or Lax) flags.',
|
|
409
|
+
}));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
} catch {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return findings;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
_getBaseUrl(surfaceInventory) {
|
|
421
|
+
const pages = surfaceInventory.pages || [];
|
|
422
|
+
if (pages.length === 0) return null;
|
|
423
|
+
try {
|
|
424
|
+
const parsed = new URL(pages[0].url || pages[0]);
|
|
425
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
426
|
+
} catch { return null; }
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export default AuthFlowTester;
|