/** * 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> = [ { __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 '', // 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, 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 = {}; 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() }; }