Download src/fuzz/api_fuzz.ts from mason-keller/vulnforge-mcp: direct link, hf CLI and curl.
- Browser
- Download file 16.7 kB
-
https://huggingface.co/mason-keller/vulnforge-mcp/resolve/main/src/fuzz/api_fuzz.ts
- Command line
-
hf download hf://mason-keller/vulnforge-mcp/src/fuzz/api_fuzz.ts
-
curl -L -o api_fuzz.ts https://huggingface.co/mason-keller/vulnforge-mcp/resolve/main/src/fuzz/api_fuzz.ts
16.7 kB
| /** | |
| * 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() | |
| }; | |
| } | |