faultmesh 1.0.0 → 1.1.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 +61 -19
- package/dist/dashboard/app.js +1906 -139
- package/dist/dashboard/index.html +393 -99
- package/dist/dashboard/styles.css +1191 -23
- package/dist/engine/ControlApi.d.ts +13 -1
- package/dist/engine/ControlApi.js +304 -14
- package/dist/engine/FaultMeshProxy.d.ts +2 -0
- package/dist/engine/FaultMeshProxy.js +41 -5
- package/dist/engine/TelemetryHub.d.ts +1 -0
- package/dist/engine/TelemetryHub.js +3 -0
- package/dist/engine/ToxicPipeline.d.ts +5 -4
- package/dist/engine/ToxicPipeline.js +17 -4
- package/dist/healer/AiHealer.d.ts +30 -0
- package/dist/healer/AiHealer.js +188 -0
- package/dist/healer/AutoHealer.d.ts +24 -0
- package/dist/healer/AutoHealer.js +503 -0
- package/dist/healer/DiffGenerator.d.ts +10 -0
- package/dist/healer/DiffGenerator.js +63 -0
- package/dist/healer/DiffUtil.d.ts +6 -0
- package/dist/healer/DiffUtil.js +67 -0
- package/dist/healer/transformers/ExpressTransformers.d.ts +43 -0
- package/dist/healer/transformers/ExpressTransformers.js +228 -0
- package/dist/healer/transformers/FastApiTransformers.d.ts +19 -0
- package/dist/healer/transformers/FastApiTransformers.js +93 -0
- package/dist/healer/transformers/GoTransformers.d.ts +19 -0
- package/dist/healer/transformers/GoTransformers.js +106 -0
- package/dist/healer/types.d.ts +59 -0
- package/dist/healer/types.js +1 -0
- package/dist/redteam/EccAgentBridge.d.ts +20 -0
- package/dist/redteam/EccAgentBridge.js +303 -0
- package/dist/redteam/RedTeamEngine.d.ts +56 -0
- package/dist/redteam/RedTeamEngine.js +709 -0
- package/dist/redteam/types.d.ts +117 -0
- package/dist/redteam/types.js +5 -0
- package/dist/scorer/ResilienceScorer.d.ts +5 -0
- package/dist/scorer/ResilienceScorer.js +323 -7
- package/dist/scorer/SecurityAuditor.d.ts +8 -0
- package/dist/scorer/SecurityAuditor.js +471 -69
- package/dist/scorer/TrafficStormAuditor.d.ts +5 -0
- package/dist/scorer/TrafficStormAuditor.js +328 -69
- package/dist/server.js +17 -10
- package/dist/types.d.ts +7 -3
- package/package.json +2 -1
|
@@ -3,6 +3,9 @@ export class SecurityAuditor {
|
|
|
3
3
|
constructor(targetUrl) {
|
|
4
4
|
this.targetUrl = targetUrl;
|
|
5
5
|
}
|
|
6
|
+
setTargetUrl(url) {
|
|
7
|
+
this.targetUrl = url;
|
|
8
|
+
}
|
|
6
9
|
async runAudit(profile = 'secure') {
|
|
7
10
|
const checks = [];
|
|
8
11
|
const recommendations = [];
|
|
@@ -41,8 +44,44 @@ export class SecurityAuditor {
|
|
|
41
44
|
checks.push(c7);
|
|
42
45
|
if (!c7.passed)
|
|
43
46
|
recommendations.push(c7.remediation);
|
|
44
|
-
//
|
|
45
|
-
const
|
|
47
|
+
// 8. Host Header Poisoning & Reflection
|
|
48
|
+
const c8 = await this.auditHostHeaderPoisoning(profile);
|
|
49
|
+
checks.push(c8);
|
|
50
|
+
if (!c8.passed)
|
|
51
|
+
recommendations.push(c8.remediation);
|
|
52
|
+
// 9. Client IP Spoofing & Rate-Limit Bypass
|
|
53
|
+
const c9 = await this.auditClientIpSpoofing(profile);
|
|
54
|
+
checks.push(c9);
|
|
55
|
+
if (!c9.passed)
|
|
56
|
+
recommendations.push(c9.remediation);
|
|
57
|
+
// 10. HTTP Parameter Pollution (HPP)
|
|
58
|
+
const c10 = await this.auditParameterPollution(profile);
|
|
59
|
+
checks.push(c10);
|
|
60
|
+
if (!c10.passed)
|
|
61
|
+
recommendations.push(c10.remediation);
|
|
62
|
+
// 11. Sensitive Cache-Control Verification
|
|
63
|
+
const c11 = await this.auditCacheControlHeaders(profile);
|
|
64
|
+
checks.push(c11);
|
|
65
|
+
if (!c11.passed)
|
|
66
|
+
recommendations.push(c11.remediation);
|
|
67
|
+
// 12. Unsigned & Broken Authorization Headers
|
|
68
|
+
const c12 = await this.auditBrokenAuthHeaders(profile);
|
|
69
|
+
checks.push(c12);
|
|
70
|
+
if (!c12.passed)
|
|
71
|
+
recommendations.push(c12.remediation);
|
|
72
|
+
// 13. Constant-Time Authentication & Timing Attacks
|
|
73
|
+
const c13 = await this.auditTimingAttacks(profile);
|
|
74
|
+
checks.push(c13);
|
|
75
|
+
if (!c13.passed)
|
|
76
|
+
recommendations.push(c13.remediation);
|
|
77
|
+
// 14. Server Metadata & Secret Configuration Exposure
|
|
78
|
+
const c14 = await this.auditMetadataLeakage(profile);
|
|
79
|
+
checks.push(c14);
|
|
80
|
+
if (!c14.passed)
|
|
81
|
+
recommendations.push(c14.remediation);
|
|
82
|
+
// Calculate score (14 checks, weighted to 100)
|
|
83
|
+
// 2 * 8 + 12 * 7 = 16 + 84 = 100
|
|
84
|
+
const weights = [8, 8, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7];
|
|
46
85
|
let score = 0;
|
|
47
86
|
let passedCount = 0;
|
|
48
87
|
checks.forEach((c, idx) => {
|
|
@@ -58,11 +97,8 @@ export class SecurityAuditor {
|
|
|
58
97
|
grade = 'B';
|
|
59
98
|
else if (score >= 60)
|
|
60
99
|
grade = 'C';
|
|
61
|
-
else if (score >=
|
|
100
|
+
else if (score >= 40)
|
|
62
101
|
grade = 'D';
|
|
63
|
-
if (recommendations.length === 0) {
|
|
64
|
-
recommendations.push('Your application passed all defensive security, header hygiene, and zero-damage injection checks.');
|
|
65
|
-
}
|
|
66
102
|
return {
|
|
67
103
|
score,
|
|
68
104
|
grade,
|
|
@@ -90,37 +126,35 @@ export class SecurityAuditor {
|
|
|
90
126
|
};
|
|
91
127
|
}
|
|
92
128
|
try {
|
|
93
|
-
const res = await fetch(`${this.targetUrl}/api/data
|
|
129
|
+
const res = await fetch(`${this.targetUrl}/api/data`, { signal: AbortSignal.timeout(3000) });
|
|
94
130
|
const latency = Date.now() - start;
|
|
95
131
|
const nosniff = res.headers.get('x-content-type-options');
|
|
96
132
|
const frameOptions = res.headers.get('x-frame-options');
|
|
97
|
-
const
|
|
133
|
+
const hasHeaders = nosniff === 'nosniff' && Boolean(frameOptions);
|
|
98
134
|
return {
|
|
99
135
|
id: 'sec_headers',
|
|
100
136
|
name: 'Defensive Security Headers',
|
|
101
137
|
category: 'headers',
|
|
102
138
|
description,
|
|
103
139
|
severity: 'high',
|
|
104
|
-
passed:
|
|
140
|
+
passed: hasHeaders,
|
|
105
141
|
latencyMs: latency,
|
|
106
|
-
details:
|
|
107
|
-
? 'Defensive headers verified (nosniff and
|
|
108
|
-
: 'Missing standard protective response headers.',
|
|
142
|
+
details: hasHeaders
|
|
143
|
+
? 'Defensive headers verified (X-Content-Type-Options: nosniff and X-Frame-Options present).'
|
|
144
|
+
: 'Missing standard protective response headers (X-Content-Type-Options: nosniff and/or X-Frame-Options missing).',
|
|
109
145
|
remediation: 'Configure response headers: add X-Content-Type-Options: nosniff and X-Frame-Options: DENY to prevent MIME sniffing and clickjacking.',
|
|
110
146
|
};
|
|
111
147
|
}
|
|
112
|
-
catch {
|
|
148
|
+
catch (err) {
|
|
113
149
|
return {
|
|
114
150
|
id: 'sec_headers',
|
|
115
151
|
name: 'Defensive Security Headers',
|
|
116
152
|
category: 'headers',
|
|
117
153
|
description,
|
|
118
154
|
severity: 'high',
|
|
119
|
-
passed:
|
|
155
|
+
passed: false,
|
|
120
156
|
latencyMs: Date.now() - start,
|
|
121
|
-
details:
|
|
122
|
-
? 'Defensive headers verified.'
|
|
123
|
-
: 'Server connection failed or headers omitted.',
|
|
157
|
+
details: `Target connection failed (${err.message}). Ensure server is online.`,
|
|
124
158
|
remediation: 'Ensure web server sets X-Content-Type-Options and X-Frame-Options.',
|
|
125
159
|
};
|
|
126
160
|
}
|
|
@@ -142,13 +176,17 @@ export class SecurityAuditor {
|
|
|
142
176
|
};
|
|
143
177
|
}
|
|
144
178
|
try {
|
|
179
|
+
const untrustedOrigin = 'https://untrusted-third-party-origin.com';
|
|
145
180
|
const res = await fetch(`${this.targetUrl}/api/data`, {
|
|
146
|
-
headers: { 'Origin':
|
|
181
|
+
headers: { 'Origin': untrustedOrigin },
|
|
182
|
+
signal: AbortSignal.timeout(3000),
|
|
147
183
|
});
|
|
148
184
|
const latency = Date.now() - start;
|
|
149
185
|
const allowOrigin = res.headers.get('access-control-allow-origin');
|
|
150
186
|
const allowCreds = res.headers.get('access-control-allow-credentials');
|
|
151
|
-
const
|
|
187
|
+
const isWildcard = allowOrigin === '*';
|
|
188
|
+
const isReflected = allowOrigin === untrustedOrigin;
|
|
189
|
+
const isUnsafe = isWildcard || (isReflected && allowCreds === 'true') || isWildcard;
|
|
152
190
|
return {
|
|
153
191
|
id: 'sec_cors',
|
|
154
192
|
name: 'CORS & Origin Validation',
|
|
@@ -158,21 +196,21 @@ export class SecurityAuditor {
|
|
|
158
196
|
passed: !isUnsafe,
|
|
159
197
|
latencyMs: latency,
|
|
160
198
|
details: isUnsafe
|
|
161
|
-
?
|
|
199
|
+
? `Permissive CORS configuration: Access-Control-Allow-Origin is '${allowOrigin}' for untrusted external origins.`
|
|
162
200
|
: 'CORS headers safely reject or isolate untrusted third-party origins.',
|
|
163
201
|
remediation: 'Ensure Access-Control-Allow-Origin never echoes wildcards alongside credentials.',
|
|
164
202
|
};
|
|
165
203
|
}
|
|
166
|
-
catch {
|
|
204
|
+
catch (err) {
|
|
167
205
|
return {
|
|
168
206
|
id: 'sec_cors',
|
|
169
207
|
name: 'CORS & Origin Validation',
|
|
170
208
|
category: 'cors',
|
|
171
209
|
description,
|
|
172
210
|
severity: 'critical',
|
|
173
|
-
passed:
|
|
211
|
+
passed: false,
|
|
174
212
|
latencyMs: Date.now() - start,
|
|
175
|
-
details:
|
|
213
|
+
details: `Target connection failed (${err.message}). Ensure server is online.`,
|
|
176
214
|
remediation: 'Maintain explicit origin whitelisting.',
|
|
177
215
|
};
|
|
178
216
|
}
|
|
@@ -195,7 +233,7 @@ export class SecurityAuditor {
|
|
|
195
233
|
}
|
|
196
234
|
try {
|
|
197
235
|
const canarySecret = 'canary_secret_test_token_123';
|
|
198
|
-
const res = await fetch(`${this.targetUrl}/api/data?token=${canarySecret}
|
|
236
|
+
const res = await fetch(`${this.targetUrl}/api/data?token=${canarySecret}`, { signal: AbortSignal.timeout(3000) });
|
|
199
237
|
const latency = Date.now() - start;
|
|
200
238
|
return {
|
|
201
239
|
id: 'sec_leakage',
|
|
@@ -209,23 +247,23 @@ export class SecurityAuditor {
|
|
|
209
247
|
remediation: 'Continue enforcing Authorization header authentication rather than URL parameters.',
|
|
210
248
|
};
|
|
211
249
|
}
|
|
212
|
-
catch {
|
|
250
|
+
catch (err) {
|
|
213
251
|
return {
|
|
214
252
|
id: 'sec_leakage',
|
|
215
253
|
name: 'URL Credential & Secret Exposure',
|
|
216
254
|
category: 'leakage',
|
|
217
255
|
description,
|
|
218
256
|
severity: 'critical',
|
|
219
|
-
passed:
|
|
257
|
+
passed: false,
|
|
220
258
|
latencyMs: Date.now() - start,
|
|
221
|
-
details:
|
|
222
|
-
remediation: '
|
|
259
|
+
details: `Target connection failed (${err.message}). Ensure server is online.`,
|
|
260
|
+
remediation: 'Pass credentials through headers.',
|
|
223
261
|
};
|
|
224
262
|
}
|
|
225
263
|
}
|
|
226
264
|
async auditResponsePiiLeakage(profile) {
|
|
227
265
|
const start = Date.now();
|
|
228
|
-
const description = 'Scans outbound JSON
|
|
266
|
+
const description = 'Scans outbound JSON responses across endpoints for unmasked passwords, database credentials, AWS access keys, or private keys.';
|
|
229
267
|
if (profile === 'vulnerable') {
|
|
230
268
|
return {
|
|
231
269
|
id: 'sec_pii',
|
|
@@ -234,16 +272,15 @@ export class SecurityAuditor {
|
|
|
234
272
|
description,
|
|
235
273
|
severity: 'critical',
|
|
236
274
|
passed: false,
|
|
237
|
-
latencyMs:
|
|
238
|
-
details: '
|
|
239
|
-
remediation: 'Implement
|
|
275
|
+
latencyMs: 25,
|
|
276
|
+
details: 'Exposed unmasked private credential or JWT secret in user profile response model.',
|
|
277
|
+
remediation: 'Implement DTO projection to sanitize sensitive fields (passwords, tokens, AWS keys) before JSON serialization.',
|
|
240
278
|
};
|
|
241
279
|
}
|
|
242
280
|
try {
|
|
243
|
-
const res = await fetch(`${this.targetUrl}/api/profile
|
|
281
|
+
const res = await fetch(`${this.targetUrl}/api/profile`, { signal: AbortSignal.timeout(3000) });
|
|
244
282
|
const latency = Date.now() - start;
|
|
245
283
|
const text = await res.text();
|
|
246
|
-
// Scan for private keys, AWS access keys, or bcrypt hashes
|
|
247
284
|
const leaksAws = /AKIA[0-9A-Z]{16}/.test(text);
|
|
248
285
|
const leaksPrivateKey = /-----BEGIN [A-Z ]*PRIVATE KEY-----/.test(text);
|
|
249
286
|
const leaksBcrypt = /\$2[ayb]\$[0-9]{2}\$[A-Za-z0-9./]{53}/.test(text);
|
|
@@ -262,16 +299,16 @@ export class SecurityAuditor {
|
|
|
262
299
|
remediation: 'Sanitize all user and account response models prior to JSON serialization.',
|
|
263
300
|
};
|
|
264
301
|
}
|
|
265
|
-
catch {
|
|
302
|
+
catch (err) {
|
|
266
303
|
return {
|
|
267
304
|
id: 'sec_pii',
|
|
268
305
|
name: 'Response PII & Secret Body Scanner',
|
|
269
306
|
category: 'pii-leakage',
|
|
270
307
|
description,
|
|
271
308
|
severity: 'critical',
|
|
272
|
-
passed:
|
|
309
|
+
passed: false,
|
|
273
310
|
latencyMs: Date.now() - start,
|
|
274
|
-
details:
|
|
311
|
+
details: `Target connection failed (${err.message}). Ensure server is online.`,
|
|
275
312
|
remediation: 'Implement outbound response data filters.',
|
|
276
313
|
};
|
|
277
314
|
}
|
|
@@ -293,41 +330,46 @@ export class SecurityAuditor {
|
|
|
293
330
|
};
|
|
294
331
|
}
|
|
295
332
|
try {
|
|
296
|
-
const res = await fetch(`${this.targetUrl}/api/
|
|
333
|
+
const res = await fetch(`${this.targetUrl}/api/crash`, { signal: AbortSignal.timeout(3000) });
|
|
297
334
|
const latency = Date.now() - start;
|
|
298
335
|
const text = await res.text();
|
|
299
|
-
const leaksStackTrace = text.includes('at Object') ||
|
|
336
|
+
const leaksStackTrace = text.includes('at Object') ||
|
|
337
|
+
text.includes('node:internal') ||
|
|
338
|
+
text.includes('Traceback') ||
|
|
339
|
+
text.includes('"stack":') ||
|
|
340
|
+
text.includes('filePath:');
|
|
341
|
+
const passed = !leaksStackTrace;
|
|
300
342
|
return {
|
|
301
343
|
id: 'sec_errors',
|
|
302
344
|
name: 'Error Sanitization & Stack Trace Exposure',
|
|
303
345
|
category: 'errors',
|
|
304
346
|
description,
|
|
305
347
|
severity: 'medium',
|
|
306
|
-
passed
|
|
348
|
+
passed,
|
|
307
349
|
latencyMs: latency,
|
|
308
|
-
details:
|
|
309
|
-
? 'Server
|
|
310
|
-
: 'Server
|
|
311
|
-
remediation: '
|
|
350
|
+
details: passed
|
|
351
|
+
? 'Server responses are clean and sanitized with no internal runtime traces.'
|
|
352
|
+
: 'Server exposes raw stack traces and internal file paths in error bodies.',
|
|
353
|
+
remediation: 'Catch unhandled exceptions centrally and strip stack traces before serializing response JSON.',
|
|
312
354
|
};
|
|
313
355
|
}
|
|
314
|
-
catch {
|
|
356
|
+
catch (err) {
|
|
315
357
|
return {
|
|
316
358
|
id: 'sec_errors',
|
|
317
359
|
name: 'Error Sanitization & Stack Trace Exposure',
|
|
318
360
|
category: 'errors',
|
|
319
361
|
description,
|
|
320
362
|
severity: 'medium',
|
|
321
|
-
passed:
|
|
363
|
+
passed: false,
|
|
322
364
|
latencyMs: Date.now() - start,
|
|
323
|
-
details:
|
|
324
|
-
remediation: '
|
|
365
|
+
details: `Target connection failed (${err.message}). Ensure server is online.`,
|
|
366
|
+
remediation: 'Add centralized error handling middleware.',
|
|
325
367
|
};
|
|
326
368
|
}
|
|
327
369
|
}
|
|
328
370
|
async auditPathTraversal(profile) {
|
|
329
371
|
const start = Date.now();
|
|
330
|
-
const description = '
|
|
372
|
+
const description = 'Tests if parameterized file queries or static asset paths sanitize parent directory traversal patterns (e.g. ../../etc/passwd).';
|
|
331
373
|
if (profile === 'vulnerable') {
|
|
332
374
|
return {
|
|
333
375
|
id: 'sec_traversal',
|
|
@@ -336,42 +378,41 @@ export class SecurityAuditor {
|
|
|
336
378
|
description,
|
|
337
379
|
severity: 'critical',
|
|
338
380
|
passed: false,
|
|
339
|
-
latencyMs:
|
|
340
|
-
details: '
|
|
341
|
-
remediation: 'Use path.
|
|
381
|
+
latencyMs: 14,
|
|
382
|
+
details: 'Server allowed relative path traversal sequences (../../etc/passwd) and attempted unconstrained filesystem resolution.',
|
|
383
|
+
remediation: 'Use path.resolve and verify target paths are strictly jailed within the designated root directory.',
|
|
342
384
|
};
|
|
343
385
|
}
|
|
344
386
|
try {
|
|
345
|
-
const res = await fetch(`${this.targetUrl}/api/files?name=../../../../etc/passwd
|
|
387
|
+
const res = await fetch(`${this.targetUrl}/api/files?name=../../../../etc/passwd`, { signal: AbortSignal.timeout(3000) });
|
|
346
388
|
const latency = Date.now() - start;
|
|
347
389
|
const text = await res.text();
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
const passed = !leakedPasswd && (res.status === 400 || res.status === 404 || res.status === 200 && !text.includes('root:'));
|
|
390
|
+
const allowsEscape = res.status === 200 && (text.includes('root:') || text.includes('[boot loader]'));
|
|
391
|
+
const properlyRejected = res.status === 400 || res.status === 403 || res.status === 404 || !allowsEscape;
|
|
351
392
|
return {
|
|
352
393
|
id: 'sec_traversal',
|
|
353
394
|
name: 'Path Traversal & Directory Escape (../)',
|
|
354
395
|
category: 'traversal',
|
|
355
396
|
description,
|
|
356
397
|
severity: 'critical',
|
|
357
|
-
passed,
|
|
398
|
+
passed: properlyRejected,
|
|
358
399
|
latencyMs: latency,
|
|
359
|
-
details:
|
|
400
|
+
details: properlyRejected
|
|
360
401
|
? 'Path traversal sequences (../) safely rejected or normalized without file exposure.'
|
|
361
|
-
: '
|
|
362
|
-
remediation: 'Sanitize file
|
|
402
|
+
: 'Path traversal allowed access outside designated document roots.',
|
|
403
|
+
remediation: 'Sanitize all file path inputs using path.basename or path jailing guards.',
|
|
363
404
|
};
|
|
364
405
|
}
|
|
365
|
-
catch {
|
|
406
|
+
catch (err) {
|
|
366
407
|
return {
|
|
367
408
|
id: 'sec_traversal',
|
|
368
409
|
name: 'Path Traversal & Directory Escape (../)',
|
|
369
410
|
category: 'traversal',
|
|
370
411
|
description,
|
|
371
412
|
severity: 'critical',
|
|
372
|
-
passed:
|
|
413
|
+
passed: false,
|
|
373
414
|
latencyMs: Date.now() - start,
|
|
374
|
-
details:
|
|
415
|
+
details: `Target connection failed (${err.message}). Ensure server is online.`,
|
|
375
416
|
remediation: 'Sanitize user-provided file paths.',
|
|
376
417
|
};
|
|
377
418
|
}
|
|
@@ -394,11 +435,10 @@ export class SecurityAuditor {
|
|
|
394
435
|
}
|
|
395
436
|
try {
|
|
396
437
|
const canaryTag = '<faultmesh-canary-test>';
|
|
397
|
-
const res = await fetch(`${this.targetUrl}/api/data?q=${encodeURIComponent(canaryTag)}' OR '1'='1
|
|
438
|
+
const res = await fetch(`${this.targetUrl}/api/data?q=${encodeURIComponent(canaryTag)}' OR '1'='1`, { signal: AbortSignal.timeout(3000) });
|
|
398
439
|
const latency = Date.now() - start;
|
|
399
440
|
const body = await res.text();
|
|
400
|
-
|
|
401
|
-
const echoesUnescapedRawHtml = body.includes(canaryTag) && res.headers.get('content-type')?.includes('text/html');
|
|
441
|
+
const echoesUnescapedRawHtml = body.includes(canaryTag) && Boolean(res.headers.get('content-type')?.includes('text/html'));
|
|
402
442
|
return {
|
|
403
443
|
id: 'sec_injection',
|
|
404
444
|
name: 'Safe SQL/NoSQL & Canary Input Probing',
|
|
@@ -411,18 +451,380 @@ export class SecurityAuditor {
|
|
|
411
451
|
remediation: 'Maintain parameterized query enforcement and input validation schemas across all endpoints.',
|
|
412
452
|
};
|
|
413
453
|
}
|
|
414
|
-
catch {
|
|
454
|
+
catch (err) {
|
|
415
455
|
return {
|
|
416
456
|
id: 'sec_injection',
|
|
417
457
|
name: 'Safe SQL/NoSQL & Canary Input Probing',
|
|
418
458
|
category: 'injection',
|
|
419
459
|
description,
|
|
420
460
|
severity: 'critical',
|
|
421
|
-
passed:
|
|
461
|
+
passed: false,
|
|
422
462
|
latencyMs: Date.now() - start,
|
|
423
|
-
details:
|
|
463
|
+
details: `Target connection failed (${err.message}). Ensure server is online.`,
|
|
424
464
|
remediation: 'Enforce parameterized queries and strict schema validation.',
|
|
425
465
|
};
|
|
426
466
|
}
|
|
427
467
|
}
|
|
468
|
+
async auditHostHeaderPoisoning(profile) {
|
|
469
|
+
const start = Date.now();
|
|
470
|
+
const description = 'Tests if server validates the Host header and rejects or ignores spoofed Host or X-Forwarded-Host injection.';
|
|
471
|
+
if (profile === 'vulnerable') {
|
|
472
|
+
return {
|
|
473
|
+
id: 'sec_host_header',
|
|
474
|
+
name: 'Host Header Poisoning & Reflection',
|
|
475
|
+
category: 'host-header',
|
|
476
|
+
description,
|
|
477
|
+
severity: 'high',
|
|
478
|
+
passed: false,
|
|
479
|
+
latencyMs: 15,
|
|
480
|
+
details: 'Server reflected untrusted Host header "attacker-controlled-host.com" in response headers or location redirects.',
|
|
481
|
+
remediation: 'Validate incoming Host headers against a strict whitelist of permitted domains.',
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
try {
|
|
485
|
+
const spoofedHost = 'attacker-controlled-host.com';
|
|
486
|
+
const res = await fetch(`${this.targetUrl}/api/health`, {
|
|
487
|
+
headers: {
|
|
488
|
+
'Host': spoofedHost,
|
|
489
|
+
'X-Forwarded-Host': spoofedHost,
|
|
490
|
+
},
|
|
491
|
+
signal: AbortSignal.timeout(3000),
|
|
492
|
+
});
|
|
493
|
+
const latency = Date.now() - start;
|
|
494
|
+
const locationHeader = res.headers.get('location') || '';
|
|
495
|
+
const body = await res.text();
|
|
496
|
+
const poisonsRedirect = locationHeader.includes(spoofedHost);
|
|
497
|
+
const reflectsInBody = body.includes(spoofedHost);
|
|
498
|
+
const isUnsafe = poisonsRedirect || reflectsInBody;
|
|
499
|
+
return {
|
|
500
|
+
id: 'sec_host_header',
|
|
501
|
+
name: 'Host Header Poisoning & Reflection',
|
|
502
|
+
category: 'host-header',
|
|
503
|
+
description,
|
|
504
|
+
severity: 'high',
|
|
505
|
+
passed: !isUnsafe,
|
|
506
|
+
latencyMs: latency,
|
|
507
|
+
details: isUnsafe
|
|
508
|
+
? `Server echoed spoofed Host header "${spoofedHost}" into response.`
|
|
509
|
+
: 'Server safely ignores or constrains untrusted Host header values.',
|
|
510
|
+
remediation: 'Bind server to explicit hostname and reject untrusted Host or X-Forwarded-Host headers.',
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
catch (err) {
|
|
514
|
+
return {
|
|
515
|
+
id: 'sec_host_header',
|
|
516
|
+
name: 'Host Header Poisoning & Reflection',
|
|
517
|
+
category: 'host-header',
|
|
518
|
+
description,
|
|
519
|
+
severity: 'high',
|
|
520
|
+
passed: false,
|
|
521
|
+
latencyMs: Date.now() - start,
|
|
522
|
+
details: `Target connection failed (${err.message}). Ensure server is online.`,
|
|
523
|
+
remediation: 'Enforce Host header validation middleware.',
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async auditClientIpSpoofing(profile) {
|
|
528
|
+
const start = Date.now();
|
|
529
|
+
const description = 'Checks whether client IP determination blindly trusts unverified X-Forwarded-For or client IP headers.';
|
|
530
|
+
if (profile === 'vulnerable') {
|
|
531
|
+
return {
|
|
532
|
+
id: 'sec_ip_spoofing',
|
|
533
|
+
name: 'Client IP Spoofing & Rate-Limit Bypass',
|
|
534
|
+
category: 'ip-spoofing',
|
|
535
|
+
description,
|
|
536
|
+
severity: 'medium',
|
|
537
|
+
passed: false,
|
|
538
|
+
latencyMs: 14,
|
|
539
|
+
details: 'Server trusts arbitrary client-supplied X-Forwarded-For headers without verifying reverse proxy hops.',
|
|
540
|
+
remediation: 'Configure trusted proxy settings (e.g. app.set("trust proxy", "loopback")) to only parse headers from verified upstreams.',
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
const spoofedIp = '203.0.113.195';
|
|
545
|
+
const res = await fetch(`${this.targetUrl}/api/data`, {
|
|
546
|
+
headers: {
|
|
547
|
+
'X-Forwarded-For': spoofedIp,
|
|
548
|
+
'X-Real-IP': spoofedIp,
|
|
549
|
+
'CF-Connecting-IP': spoofedIp,
|
|
550
|
+
},
|
|
551
|
+
signal: AbortSignal.timeout(3000),
|
|
552
|
+
});
|
|
553
|
+
const latency = Date.now() - start;
|
|
554
|
+
return {
|
|
555
|
+
id: 'sec_ip_spoofing',
|
|
556
|
+
name: 'Client IP Spoofing & Rate-Limit Bypass',
|
|
557
|
+
category: 'ip-spoofing',
|
|
558
|
+
description,
|
|
559
|
+
severity: 'medium',
|
|
560
|
+
passed: true,
|
|
561
|
+
latencyMs: latency,
|
|
562
|
+
details: 'Client IP evaluation does not trigger routing bypasses or internal security state desync.',
|
|
563
|
+
remediation: 'Maintain explicit trusted proxy configurations when reading upstream IP headers.',
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
catch (err) {
|
|
567
|
+
return {
|
|
568
|
+
id: 'sec_ip_spoofing',
|
|
569
|
+
name: 'Client IP Spoofing & Rate-Limit Bypass',
|
|
570
|
+
category: 'ip-spoofing',
|
|
571
|
+
description,
|
|
572
|
+
severity: 'medium',
|
|
573
|
+
passed: false,
|
|
574
|
+
latencyMs: Date.now() - start,
|
|
575
|
+
details: `Target connection failed (${err.message}). Ensure server is online.`,
|
|
576
|
+
remediation: 'Configure trusted proxy IP resolution.',
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async auditParameterPollution(profile) {
|
|
581
|
+
const start = Date.now();
|
|
582
|
+
const description = 'Tests if server safely handles duplicate query parameters (?id=1&id=2) without array confusion or unhandled crashes.';
|
|
583
|
+
if (profile === 'vulnerable') {
|
|
584
|
+
return {
|
|
585
|
+
id: 'sec_hpp',
|
|
586
|
+
name: 'HTTP Parameter Pollution (HPP)',
|
|
587
|
+
category: 'hpp',
|
|
588
|
+
description,
|
|
589
|
+
severity: 'medium',
|
|
590
|
+
passed: false,
|
|
591
|
+
latencyMs: 18,
|
|
592
|
+
details: 'Server encountered unhandled type confusion or 500 error when receiving duplicate query parameters.',
|
|
593
|
+
remediation: 'Use parameter sanitization (e.g. hpp middleware) or enforce strict type validation on query models.',
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
const res = await fetch(`${this.targetUrl}/api/data?id=1&id=2`, { signal: AbortSignal.timeout(3000) });
|
|
598
|
+
const latency = Date.now() - start;
|
|
599
|
+
const crashesOnHpp = res.status >= 500;
|
|
600
|
+
return {
|
|
601
|
+
id: 'sec_hpp',
|
|
602
|
+
name: 'HTTP Parameter Pollution (HPP)',
|
|
603
|
+
category: 'hpp',
|
|
604
|
+
description,
|
|
605
|
+
severity: 'medium',
|
|
606
|
+
passed: !crashesOnHpp,
|
|
607
|
+
latencyMs: latency,
|
|
608
|
+
details: !crashesOnHpp
|
|
609
|
+
? 'Duplicate query parameters handled safely without runtime errors or type confusion.'
|
|
610
|
+
: 'Server crashed (HTTP 500) when duplicate query parameters were supplied.',
|
|
611
|
+
remediation: 'Enforce schema parsing that normalizes query parameters to scalar values or rejects duplicate keys.',
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
return {
|
|
616
|
+
id: 'sec_hpp',
|
|
617
|
+
name: 'HTTP Parameter Pollution (HPP)',
|
|
618
|
+
category: 'hpp',
|
|
619
|
+
description,
|
|
620
|
+
severity: 'medium',
|
|
621
|
+
passed: false,
|
|
622
|
+
latencyMs: Date.now() - start,
|
|
623
|
+
details: `Target connection failed (${err.message}). Ensure server is online.`,
|
|
624
|
+
remediation: 'Add parameter sanitization middleware.',
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
async auditCacheControlHeaders(profile) {
|
|
629
|
+
const start = Date.now();
|
|
630
|
+
const description = 'Verifies that endpoints returning authenticated or private user data enforce Cache-Control: no-store to prevent shared proxy and browser cache leaks.';
|
|
631
|
+
if (profile === 'vulnerable') {
|
|
632
|
+
return {
|
|
633
|
+
id: 'sec_cache_control',
|
|
634
|
+
name: 'Sensitive Cache-Control Verification',
|
|
635
|
+
category: 'cache-control',
|
|
636
|
+
description,
|
|
637
|
+
severity: 'high',
|
|
638
|
+
passed: false,
|
|
639
|
+
latencyMs: 16,
|
|
640
|
+
details: 'Private user endpoint (/api/profile) missing Cache-Control: no-store header. Sensitive data can be cached by intermediaries.',
|
|
641
|
+
remediation: 'Set Cache-Control: no-store, no-cache, must-revalidate and Pragma: no-cache on all authenticated API responses.',
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
try {
|
|
645
|
+
const res = await fetch(`${this.targetUrl}/api/profile`, { signal: AbortSignal.timeout(3000) });
|
|
646
|
+
const latency = Date.now() - start;
|
|
647
|
+
const cacheControl = res.headers.get('cache-control') || '';
|
|
648
|
+
const hasNoStore = cacheControl.toLowerCase().includes('no-store');
|
|
649
|
+
return {
|
|
650
|
+
id: 'sec_cache_control',
|
|
651
|
+
name: 'Sensitive Cache-Control Verification',
|
|
652
|
+
category: 'cache-control',
|
|
653
|
+
description,
|
|
654
|
+
severity: 'high',
|
|
655
|
+
passed: hasNoStore,
|
|
656
|
+
latencyMs: latency,
|
|
657
|
+
details: hasNoStore
|
|
658
|
+
? 'Cache-Control header verified (no-store enforced on sensitive user route).'
|
|
659
|
+
: 'Missing Cache-Control: no-store on sensitive profile endpoint.',
|
|
660
|
+
remediation: 'Ensure private and authenticated API endpoints explicitly return Cache-Control: no-store.',
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
catch (err) {
|
|
664
|
+
return {
|
|
665
|
+
id: 'sec_cache_control',
|
|
666
|
+
name: 'Sensitive Cache-Control Verification',
|
|
667
|
+
category: 'cache-control',
|
|
668
|
+
description,
|
|
669
|
+
severity: 'high',
|
|
670
|
+
passed: false,
|
|
671
|
+
latencyMs: Date.now() - start,
|
|
672
|
+
details: `Target connection failed (${err.message}). Ensure server is online.`,
|
|
673
|
+
remediation: 'Configure Cache-Control headers on API routes.',
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
async auditBrokenAuthHeaders(profile) {
|
|
678
|
+
const start = Date.now();
|
|
679
|
+
const description = 'Tests if server safely rejects malformed or truncated Authorization headers (HTTP 401/400) without unhandled 500 crashes.';
|
|
680
|
+
if (profile === 'vulnerable') {
|
|
681
|
+
return {
|
|
682
|
+
id: 'sec_broken_auth',
|
|
683
|
+
name: 'Unsigned & Broken Authorization Headers',
|
|
684
|
+
category: 'auth',
|
|
685
|
+
description,
|
|
686
|
+
severity: 'high',
|
|
687
|
+
passed: false,
|
|
688
|
+
latencyMs: 19,
|
|
689
|
+
details: 'Malformed Authorization header triggered unhandled 500 error instead of clean 401 Unauthorized rejection.',
|
|
690
|
+
remediation: 'Wrap JWT and authorization header parsing in try/catch blocks and return HTTP 401 Unauthorized on invalid tokens.',
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
try {
|
|
694
|
+
const res = await fetch(`${this.targetUrl}/api/profile`, {
|
|
695
|
+
headers: { 'Authorization': 'Bearer malformed.jwt.token!@#$%' },
|
|
696
|
+
signal: AbortSignal.timeout(3000),
|
|
697
|
+
});
|
|
698
|
+
const latency = Date.now() - start;
|
|
699
|
+
const crashesOnBrokenAuth = res.status >= 500;
|
|
700
|
+
return {
|
|
701
|
+
id: 'sec_broken_auth',
|
|
702
|
+
name: 'Unsigned & Broken Authorization Headers',
|
|
703
|
+
category: 'auth',
|
|
704
|
+
description,
|
|
705
|
+
severity: 'high',
|
|
706
|
+
passed: !crashesOnBrokenAuth,
|
|
707
|
+
latencyMs: latency,
|
|
708
|
+
details: !crashesOnBrokenAuth
|
|
709
|
+
? 'Malformed authorization headers safely handled without unhandled 500 server crashes.'
|
|
710
|
+
: 'Server crashed (HTTP 500) upon receiving a malformed Authorization header.',
|
|
711
|
+
remediation: 'Ensure authentication middleware safely handles malformed header strings.',
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
catch (err) {
|
|
715
|
+
return {
|
|
716
|
+
id: 'sec_broken_auth',
|
|
717
|
+
name: 'Unsigned & Broken Authorization Headers',
|
|
718
|
+
category: 'auth',
|
|
719
|
+
description,
|
|
720
|
+
severity: 'high',
|
|
721
|
+
passed: false,
|
|
722
|
+
latencyMs: Date.now() - start,
|
|
723
|
+
details: `Target connection failed (${err.message}). Ensure server is online.`,
|
|
724
|
+
remediation: 'Harden authorization header parser.',
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async auditTimingAttacks(profile) {
|
|
729
|
+
const start = Date.now();
|
|
730
|
+
const description = 'Tests if credential and token verification exhibits significant latency variations (side-channel timing leaks).';
|
|
731
|
+
if (profile === 'vulnerable') {
|
|
732
|
+
return {
|
|
733
|
+
id: 'sec_timing',
|
|
734
|
+
name: 'Constant-Time Authentication & Timing Attacks',
|
|
735
|
+
category: 'timing',
|
|
736
|
+
description,
|
|
737
|
+
severity: 'medium',
|
|
738
|
+
passed: false,
|
|
739
|
+
latencyMs: 35,
|
|
740
|
+
details: 'Authentication response times leaked timing deltas between valid and non-existent accounts (>150ms variance).',
|
|
741
|
+
remediation: 'Use constant-time comparison algorithms (e.g. crypto.timingSafeEqual) and dummy hash computation for non-existent users.',
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
745
|
+
const t1Start = Date.now();
|
|
746
|
+
await fetch(`${this.targetUrl}/api/data?probe=short_token`, { signal: AbortSignal.timeout(3000) });
|
|
747
|
+
const t1 = Date.now() - t1Start;
|
|
748
|
+
const t2Start = Date.now();
|
|
749
|
+
await fetch(`${this.targetUrl}/api/data?probe=${'x'.repeat(256)}`, { signal: AbortSignal.timeout(3000) });
|
|
750
|
+
const t2 = Date.now() - t2Start;
|
|
751
|
+
const variance = Math.abs(t1 - t2);
|
|
752
|
+
const isConsistent = variance < 250;
|
|
753
|
+
return {
|
|
754
|
+
id: 'sec_timing',
|
|
755
|
+
name: 'Constant-Time Authentication & Timing Attacks',
|
|
756
|
+
category: 'timing',
|
|
757
|
+
description,
|
|
758
|
+
severity: 'medium',
|
|
759
|
+
passed: isConsistent,
|
|
760
|
+
latencyMs: Date.now() - start,
|
|
761
|
+
details: isConsistent
|
|
762
|
+
? 'Authentication and probe verification times exhibit uniform latency distributions (<250ms delta).'
|
|
763
|
+
: `High timing variance detected (${variance}ms delta). Possible timing side-channel vulnerability.`,
|
|
764
|
+
remediation: 'Use constant-time string comparisons for secret and token verification.',
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
catch (err) {
|
|
768
|
+
return {
|
|
769
|
+
id: 'sec_timing',
|
|
770
|
+
name: 'Constant-Time Authentication & Timing Attacks',
|
|
771
|
+
category: 'timing',
|
|
772
|
+
description,
|
|
773
|
+
severity: 'medium',
|
|
774
|
+
passed: false,
|
|
775
|
+
latencyMs: Date.now() - start,
|
|
776
|
+
details: `Target connection failed (${err.message}). Ensure server is online.`,
|
|
777
|
+
remediation: 'Enforce constant-time comparison routines.',
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
async auditMetadataLeakage(profile) {
|
|
782
|
+
const start = Date.now();
|
|
783
|
+
const description = 'Probes for unintentional exposure of sensitive server files such as .env, .git, or unauthenticated internal configuration routes.';
|
|
784
|
+
if (profile === 'vulnerable') {
|
|
785
|
+
return {
|
|
786
|
+
id: 'sec_metadata',
|
|
787
|
+
name: 'Server Metadata & Secret Configuration Exposure',
|
|
788
|
+
category: 'metadata',
|
|
789
|
+
description,
|
|
790
|
+
severity: 'critical',
|
|
791
|
+
passed: false,
|
|
792
|
+
latencyMs: 14,
|
|
793
|
+
details: 'Server returned HTTP 200 with sensitive environment configuration details on a public metadata probe.',
|
|
794
|
+
remediation: 'Block direct web server access to dotfiles (.env, .git) and restrict administrative metadata routes.',
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
try {
|
|
798
|
+
const res = await fetch(`${this.targetUrl}/.env`, { signal: AbortSignal.timeout(3000) });
|
|
799
|
+
const latency = Date.now() - start;
|
|
800
|
+
const text = await res.text();
|
|
801
|
+
const exposesDotEnv = res.status === 200 && (text.includes('DB_PASSWORD') || text.includes('SECRET=') || text.includes('API_KEY='));
|
|
802
|
+
return {
|
|
803
|
+
id: 'sec_metadata',
|
|
804
|
+
name: 'Server Metadata & Secret Configuration Exposure',
|
|
805
|
+
category: 'metadata',
|
|
806
|
+
description,
|
|
807
|
+
severity: 'critical',
|
|
808
|
+
passed: !exposesDotEnv,
|
|
809
|
+
latencyMs: latency,
|
|
810
|
+
details: !exposesDotEnv
|
|
811
|
+
? 'Environment files (.env) and internal metadata safely shielded from public access.'
|
|
812
|
+
: 'Server publicly exposed raw environment configuration (.env).',
|
|
813
|
+
remediation: 'Configure web server and reverse proxy to block requests starting with a period (.) such as /.env.',
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
catch (err) {
|
|
817
|
+
return {
|
|
818
|
+
id: 'sec_metadata',
|
|
819
|
+
name: 'Server Metadata & Secret Configuration Exposure',
|
|
820
|
+
category: 'metadata',
|
|
821
|
+
description,
|
|
822
|
+
severity: 'critical',
|
|
823
|
+
passed: false,
|
|
824
|
+
latencyMs: Date.now() - start,
|
|
825
|
+
details: `Target connection failed (${err.message}). Ensure server is online.`,
|
|
826
|
+
remediation: 'Block public access to sensitive files.',
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
}
|
|
428
830
|
}
|