File size: 16,663 Bytes
36e5a71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
/**
 * VulnForge MCP - Dynamic API Fuzzing & Mutation Resilience Auditor
 * @module fuzz/api_fuzz
 */

import {
  FuzzAuditInput,
  FuzzAuditResult,
  FuzzAuditSummary,
  FuzzMutationCase,
  FuzzMutationType,
  FuzzStrategy,
  SecurityFinding
} from '../types/index.js';

export const BOUNDARY_INTEGER_PAYLOADS = [
  0,
  -1,
  1,
  2147483647, // 32-bit signed MaxInt
  -2147483648, // 32-bit signed MinInt
  4294967295, // 32-bit unsigned MaxInt
  Number.MAX_SAFE_INTEGER, // 9007199254740991
  Number.MIN_SAFE_INTEGER, // -9007199254740991
  1e20,
  NaN,
  Infinity,
  -Infinity
];

export const PROTOTYPE_POLLUTION_PAYLOADS: Array<Record<string, any>> = [
  { __proto__: { polluted: true } },
  { constructor: { prototype: { admin: true } } },
  { 'prototype': { 'isAdmin': true } },
  { '__proto__.polluted': 'true' }
];

export const TYPE_CONFUSION_PAYLOADS = [
  null,
  undefined,
  true,
  false,
  [],
  {},
  { $gt: '' }, // NoSQL operator injection probe
  { $ne: null },
  [1, 2, 3],
  [['nested']]
];

export const MALFORMED_ENCODING_PAYLOADS = [
  '%00', // Null byte injection
  '\u0000',
  '\r\nSet-Cookie: vulnforge_session=hijacked', // CRLF header injection probe
  '../../../../etc/passwd', // Path traversal probe
  '<script>alert(1)</script>', // XSS probe
  "' OR 1=1 --", // SQLi probe
  '\uFEFF', // Zero-width non-breaking space
  'A'.repeat(10000) // Oversized string DoS payload
];

/**
 * Generates an automated suite of feedback-guided mutations for API endpoint fields.
 */
export function generateMutationSuite(
  fields: Record<string, string>,
  strategy: FuzzStrategy = 'ALL',
  rounds: number = 10
): FuzzMutationCase[] {
  const mutations: FuzzMutationCase[] = [];
  let caseId = 1;

  for (const [field, expectedType] of Object.entries(fields)) {
    // 1. Boundary Integer Mutations
    if (
      (strategy === 'ALL' || strategy === 'BOUNDARY_STRESS' || strategy === 'GENETIC_MUTATION') &&
      (expectedType === 'number' || expectedType === 'integer')
    ) {
      for (const val of BOUNDARY_INTEGER_PAYLOADS.slice(0, rounds)) {
        mutations.push({
          id: `FUZZ-INT-${caseId++}`,
          field,
          mutationType: 'BOUNDARY_INT',
          payload: val,
          description: `Boundary integer mutation probe: ${String(val)} for field '${field}'`,
          expectedStatus: 400
        });
      }
    }

    // 2. Type Confusion Mutations
    if (strategy === 'ALL' || strategy === 'TYPE_CONFUSION' || strategy === 'GENETIC_MUTATION') {
      for (const val of TYPE_CONFUSION_PAYLOADS.slice(0, Math.min(rounds, TYPE_CONFUSION_PAYLOADS.length))) {
        mutations.push({
          id: `FUZZ-TYPE-${caseId++}`,
          field,
          mutationType: 'TYPE_CONFUSION',
          payload: val,
          description: `Type confusion mutation probe with payload type '${typeof val}' for field '${field}'`,
          expectedStatus: 400
        });
      }
    }

    // 3. Prototype Pollution Probes
    if (strategy === 'ALL' || strategy === 'PROTOTYPE_POLLUTION') {
      for (const val of PROTOTYPE_POLLUTION_PAYLOADS) {
        mutations.push({
          id: `FUZZ-PROTO-${caseId++}`,
          field,
          mutationType: 'PROTOTYPE_POLLUTION',
          payload: val,
          description: `Prototype pollution injection probe into field '${field}'`,
          expectedStatus: 400
        });
      }
    }

    // 4. Malformed Encoding & Special Characters
    if (strategy === 'ALL' || strategy === 'BOUNDARY_STRESS') {
      for (const val of MALFORMED_ENCODING_PAYLOADS.slice(0, rounds)) {
        mutations.push({
          id: `FUZZ-ENC-${caseId++}`,
          field,
          mutationType:
            typeof val === 'string' && val.length > 500
              ? 'OVERSIZED_PAYLOAD'
              : 'MALFORMED_ENCODING',
          payload: val,
          description:
            typeof val === 'string' && val.length > 500
              ? `Oversized string buffer overflow / DoS stress test on field '${field}'`
              : `Encoding anomaly and delimiter injection probe on field '${field}'`,
          expectedStatus: 400
        });
      }
    }
  }

  return mutations;
}

/**
 * Audits API route source code and schemas for dynamic fuzzing resilience,
 * missing input validation, unhandled exceptions, prototype pollution, and DoS vectors.
 */
export function auditApiFuzzResilience(input: FuzzAuditInput): FuzzAuditResult {
  const code = input.routeCode || '';
  const findings: SecurityFinding[] = [];
  const targetFile = input.filePath || 'api_handler.ts';

  let totalMutationsGenerated = 0;
  let unhandledExceptionRisks = 0;
  let prototypePollutionRisks = 0;
  let missingSchemaValidationCount = 0;
  let oversizedPayloadDosRisks = 0;
  let typeConfusionRisks = 0;
  let endpointCount = 0;

  // 1. Detect API Route Handlers
  const routeRegex = /(?:app|router|server|fastify)\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
  const routeMatches = [...code.matchAll(routeRegex)];
  endpointCount = routeMatches.length || (code.trim() ? 1 : 0);

  // 2. Check for Missing Schema Validation (Zod, Joi, Yup, Celebrate, Schema Validator)
  const hasSchemaValidation =
    /\b(?:z\.object|Joi\.object|yup\.object|validate\(|validator\.|schema\.parse|schema\.safeParse|checkSchema)\b/i.test(
      code
    );

  // Check if endpoints read req.body, req.query, or req.params without validation
  const readsRequestBody = /\breq\.body(?:\.[a-zA-Z0-9_$]+)?\b/.test(code);
  const readsRequestQuery = /\breq\.query(?:\.[a-zA-Z0-9_$]+)?\b/.test(code);

  if ((readsRequestBody || readsRequestQuery) && !hasSchemaValidation) {
    missingSchemaValidationCount++;
    findings.push({
      id: 'API-FUZZ-MISSING-SCHEMA-VALIDATION',
      title: 'Missing Request Body & Parameter Schema Validation (CWE-20)',
      severity: 'HIGH',
      category: 'api_fuzzing',
      cwe: 'CWE-20: Improper Input Validation',
      target: targetFile,
      description:
        'The API route handler extracts untrusted properties directly from `req.body` or `req.query` without strict schema-level validation (e.g. Zod, Joi). Mutated payloads, type-confused inputs, or unexpected boundary primitives will bypass business assumptions.',
      evidence: readsRequestBody ? 'Direct read of `req.body` without Zod/Joi validation' : 'Direct read of `req.query` without validation',
      impact:
        'Allows attackers to trigger unexpected control flow, type errors, null-pointer dereferences, or logical bypasses by submitting mutated JSON types.',
      remediation:
        'Implement strict Zod/Joi schema parsing on incoming request bodies and reject undeclared fields using `.strict()`.',
      patchRecommendation: {
        patchType: 'VALIDATION_SCHEMA_INJECTION',
        targetFile,
        vulnerableSnippet: 'const { username, amount } = req.body;',
        patchedSnippet:
          'const RequestSchema = z.object({ username: z.string().min(1).max(100), amount: z.number().int().positive() }).strict();\nconst { username, amount } = RequestSchema.parse(req.body);',
        diff: '+ const RequestSchema = z.object({ ... }).strict();\n+ const validated = RequestSchema.parse(req.body);',
        explanation: 'Enforce runtime Zod schema parsing and type coercion to reject mutated payloads.'
      }
    });
  }

  // 3. Check for Prototype Pollution Risks (CWE-1321)
  const hasUnsafeMerge =
    /Object\.assign\s*\(\s*(?:\{\}|this|[a-zA-Z0-9_$]+)\s*,\s*(?:req\.body|data|input)\s*\)/.test(code) ||
    /(?:merge|cloneDeep|extend)\s*\(\s*(?:\{\}|[a-zA-Z0-9_$]+)\s*,\s*req\.body\s*\)/.test(code) ||
    /for\s*\(\s*(?:let|const|var)\s+([a-zA-Z0-9_$]+)\s+in\s+req\.body\s*\)\s*\{[\s\S]*?\[\1\]\s*=/.test(code);

  const hasProtoFilter =
    code.includes('__proto__') && (code.includes('continue') || code.includes('delete') || code.includes('hasOwn'));

  if (hasUnsafeMerge && !hasProtoFilter) {
    prototypePollutionRisks++;
    findings.push({
      id: 'API-FUZZ-PROTOTYPE-POLLUTION',
      title: 'Potential Prototype Pollution via Unsafe Object Mutation (CWE-1321)',
      severity: 'CRITICAL',
      category: 'api_fuzzing',
      cwe: 'CWE-1321: Improperly Controlled Modification of Object Prototype Attributes',
      target: targetFile,
      description:
        'The API endpoint recursively merges or copies properties from untrusted request data directly into objects without sanitizing `__proto__`, `constructor`, or `prototype` keys.',
      evidence: 'Unchecked `Object.assign` or iterative merge with untrusted `req.body`',
      impact:
        'Enables attackers to poison `Object.prototype`, leading to remote code execution, authentication bypass, or denial of service across all server operations.',
      remediation:
        'Explicitly reject or strip `__proto__`, `constructor`, and `prototype` keys, or use `Object.create(null)` for dictionary lookups.',
      patchRecommendation: {
        patchType: 'PROTOTYPE_POLLUTION_GUARD',
        targetFile,
        vulnerableSnippet: 'Object.assign(target, req.body);',
        patchedSnippet:
          'const safeBody = JSON.parse(JSON.stringify(req.body, (key, value) => (key === "__proto__" || key === "constructor" ? undefined : value)));\nObject.assign(target, safeBody);',
        diff: '+ const safeBody = JSON.parse(JSON.stringify(req.body, (k, v) => (k === "__proto__" || k === "constructor" ? undefined : v)));\n- Object.assign(target, req.body);\n+ Object.assign(target, safeBody);',
        explanation: 'Filter out __proto__ and constructor properties during JSON deserialization.'
      }
    });
  }

  // 4. Check for Unhandled Async Exceptions / Missing Error Boundary (CWE-754 / CWE-248)
  const isAsyncRoute = /async\s*\(\s*req\s*,\s*res\b/.test(code) || /async\s+function\s*\(\s*req\s*,\s*res\b/.test(code);
  const hasTryCatch = /try\s*\{[\s\S]*?\}\s*catch\b/.test(code);
  const hasAsyncWrapper = /asyncHandler\s*\(/.test(code) || /wrapAsync\s*\(/.test(code);

  if (isAsyncRoute && !hasTryCatch && !hasAsyncWrapper) {
    unhandledExceptionRisks++;
    findings.push({
      id: 'API-FUZZ-UNHANDLED-ASYNC-EXCEPTION',
      title: 'Unhandled Async Exception Risk in Route Handler (CWE-754)',
      severity: 'MEDIUM',
      category: 'api_fuzzing',
      cwe: 'CWE-754: Improper Check for Unusual or Exceptional Conditions',
      target: targetFile,
      description:
        'The route handler executes asynchronous operations without a `try/catch` block or an `asyncHandler` wrapper. An unhandled promise rejection caused by malformed input will terminate the Node.js process or cause an unhandled 500 error leaking internals.',
      evidence: '`async (req, res)` handler lacking `try/catch` or `asyncHandler` boundary',
      impact:
        'Process crash or unhandled 500 internal server error leading to denial of service upon receiving fuzzing mutation vectors.',
      remediation:
        'Wrap asynchronous endpoint logic in a `try/catch` block forwarding errors to `next(err)`, or use `express-async-errors`.',
      patchRecommendation: {
        patchType: 'WRAPPER',
        targetFile,
        vulnerableSnippet: 'app.post("/api", async (req, res) => {\n  const data = await service.process(req.body);\n  res.json(data);\n});',
        patchedSnippet:
          'app.post("/api", async (req, res, next) => {\n  try {\n    const data = await service.process(req.body);\n    res.json(data);\n  } catch (err) {\n    next(err);\n  }\n});',
        diff: '+ try {\n  ...\n+ } catch (err) { next(err); }',
        explanation: 'Enclose asynchronous operations in a structured error boundary.'
      }
    });
  }

  // 5. Check for Missing Body Parser Size Limit / DoS Risk (CWE-400)
  const hasBodyParserWithoutLimit =
    /(?:express\.json|bodyParser\.json|bodyParser\.urlencoded)\s*\(\s*\)/.test(code) &&
    !code.includes('limit:');

  if (hasBodyParserWithoutLimit) {
    oversizedPayloadDosRisks++;
    findings.push({
      id: 'API-FUZZ-BODY-PARSER-UNRESTRICTED-SIZE',
      title: 'Unrestricted JSON Body Parser Size Limit (CWE-400)',
      severity: 'MEDIUM',
      category: 'api_fuzzing',
      cwe: 'CWE-400: Uncontrolled Resource Consumption',
      target: targetFile,
      description:
        'The body parser middleware is initialized with default unconstrained or high limits without an explicit maximum request size (e.g. `limit: "100kb"`). Oversized mutated payloads will exhaust heap memory and cause denial of service.',
      evidence: '`express.json()` or `bodyParser.json()` initialized without explicit `limit` option',
      impact:
        'Allows attackers to trigger Node.js Out-Of-Memory (OOM) process crashes using bloated JSON buffers.',
      remediation: 'Configure an explicit payload limit on body parsing middlewares (e.g. `express.json({ limit: "100kb" })`).',
      patchRecommendation: {
        patchType: 'CONFIG_UPDATE',
        targetFile,
        vulnerableSnippet: 'app.use(express.json());',
        patchedSnippet: "app.use(express.json({ limit: '100kb' }));",
        diff: "- app.use(express.json());\n+ app.use(express.json({ limit: '100kb' }));",
        explanation: 'Restrict maximum JSON request body payload size to 100kb.'
      }
    });
  }

  // 6. Check for Missing Numeric Bounds / Integer Overflow (CWE-190)
  const hasUnboundedInt =
    /parseInt\s*\(\s*(?:req\.body|req\.query|req\.params)\.[a-zA-Z0-9_$]+\s*\)/.test(code) &&
    !code.includes('Number.isSafeInteger') &&
    !code.includes('isNaN') &&
    !code.includes('Math.min');

  if (hasUnboundedInt) {
    typeConfusionRisks++;
    findings.push({
      id: 'API-FUZZ-UNBOUNDED-INTEGER-PARSE',
      title: 'Unbounded Integer Parsing and Lack of Range Validation (CWE-190)',
      severity: 'LOW',
      category: 'api_fuzzing',
      cwe: 'CWE-190: Integer Overflow or Wraparound',
      target: targetFile,
      description:
        'Untrusted input is parsed with `parseInt()` without verifying `Number.isSafeInteger()` or enforcing minimum/maximum numerical boundaries.',
      evidence: '`parseInt(req.body.id)` called without range bounds or NaN check',
      impact:
        'Large boundary numbers (`1e308`, negative values, NaN) can distort downstream financial logic, pagination offsets, or buffer indexing.',
      remediation: 'Enforce boundary limits using `Math.max(1, Math.min(val, 1000))` and verify `Number.isSafeInteger()`.',
      patchRecommendation: {
        patchType: 'VALIDATION_RULE',
        targetFile,
        vulnerableSnippet: 'const limit = parseInt(req.query.limit);',
        patchedSnippet:
          'const rawLimit = parseInt(req.query.limit as string, 10);\nconst limit = Number.isSafeInteger(rawLimit) ? Math.max(1, Math.min(rawLimit, 100)) : 20;',
        diff: '+ const rawLimit = parseInt(req.query.limit, 10);\n+ const limit = Number.isSafeInteger(rawLimit) ? Math.max(1, Math.min(rawLimit, 100)) : 20;',
        explanation: 'Clamp parsed integer within safe operational boundaries.'
      }
    });
  }

  // Generate Sample Mutation Suite for Reporting
  const inferredFields: Record<string, string> = {};
  const propMatches = [...code.matchAll(/req\.(?:body|query)\.([a-zA-Z0-9_$]+)/g)];
  for (const m of propMatches) {
    const name = m[1];
    inferredFields[name] = /(?:id|count|amount|limit|offset|page|size)/i.test(name) ? 'number' : 'string';
  }
  if (Object.keys(inferredFields).length === 0) {
    inferredFields['payload'] = 'string';
    inferredFields['id'] = 'number';
  }

  const mutationsSample = generateMutationSuite(
    inferredFields,
    input.fuzzStrategy || 'ALL',
    input.mutationRounds || 5
  );
  totalMutationsGenerated = mutationsSample.length;

  const criticalCount = findings.filter((f) => f.severity === 'CRITICAL').length;
  const highCount = findings.filter((f) => f.severity === 'HIGH').length;
  const mediumCount = findings.filter((f) => f.severity === 'MEDIUM').length;
  const lowCount = findings.filter((f) => f.severity === 'LOW').length;

  const complianceStatus: 'PASS' | 'WARN' | 'FAIL' =
    criticalCount > 0 || highCount > 0 ? 'FAIL' : mediumCount > 0 || lowCount > 0 ? 'WARN' : 'PASS';

  const summary: FuzzAuditSummary = {
    endpointsAudited: endpointCount,
    totalMutationsGenerated,
    unhandledExceptionRisks,
    prototypePollutionRisks,
    missingSchemaValidationCount,
    oversizedPayloadDosRisks,
    typeConfusionRisks
  };

  return {
    endpointCount,
    totalFindings: findings.length,
    criticalCount,
    highCount,
    mediumCount,
    lowCount,
    findings,
    summary,
    mutationsSample: mutationsSample.slice(0, 15),
    complianceStatus,
    timestamp: new Date().toISOString()
  };
}