vulnforge-mcp / tests /api_fuzz.test.js
mason-keller
feat: AI dynamic API fuzzing & mutation resilience auditor (v1.6.0)
36e5a71
Raw History Blame Contribute Delete
10.1 kB
import test from 'node:test';
import assert from 'node:assert/strict';
import {
generateMutationSuite,
auditApiFuzzResilience,
BOUNDARY_INTEGER_PAYLOADS,
PROTOTYPE_POLLUTION_PAYLOADS
} from '../dist/fuzz/api_fuzz.js';
import { synthesizeAndInjectPatch, synthesizeAndVerifyPatch } from '../dist/patch/patch_engine.js';
import { runInCanarySandbox } from '../dist/sandbox/canary_sandbox.js';
import { executeCanaryVerification } from '../dist/sandbox/canary_runner.js';
import { createVulnForgeServer } from '../dist/mcp/server.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
test('Fuzz Generator: generateMutationSuite generates all strategy test cases', () => {
const fields = { id: 'number', name: 'string', metadata: 'object' };
const mutations = generateMutationSuite(fields, 'ALL', 5);
assert.ok(mutations.length > 10, 'Should generate multiple mutation cases');
assert.ok(
mutations.some((m) => m.mutationType === 'BOUNDARY_INT'),
'Should include boundary integer mutations'
);
assert.ok(
mutations.some((m) => m.mutationType === 'TYPE_CONFUSION'),
'Should include type confusion mutations'
);
assert.ok(
mutations.some((m) => m.mutationType === 'PROTOTYPE_POLLUTION'),
'Should include prototype pollution mutations'
);
assert.ok(
mutations.some((m) => m.mutationType === 'OVERSIZED_PAYLOAD' || m.mutationType === 'MALFORMED_ENCODING'),
'Should include encoding / oversized payload mutations'
);
});
test('API Fuzz Auditor: Detects missing schema validation on request body (CWE-20)', () => {
const code = `
app.post('/api/checkout', (req, res) => {
const { itemId, quantity, userToken } = req.body;
res.json({ status: 'ok', itemId, quantity });
});
`;
const result = auditApiFuzzResilience({ routeCode: code });
assert.equal(result.complianceStatus, 'FAIL');
assert.ok(
result.findings.some((f) => f.id === 'API-FUZZ-MISSING-SCHEMA-VALIDATION'),
'Should report missing schema validation finding'
);
assert.equal(result.summary.missingSchemaValidationCount, 1);
});
test('API Fuzz Auditor: Detects prototype pollution in unsafe object merge (CWE-1321)', () => {
const code = `
app.post('/api/settings', (req, res) => {
const userSettings = {};
Object.assign(userSettings, req.body);
res.json({ updated: true });
});
`;
const result = auditApiFuzzResilience({ routeCode: code });
assert.equal(result.complianceStatus, 'FAIL');
assert.ok(
result.findings.some((f) => f.id === 'API-FUZZ-PROTOTYPE-POLLUTION'),
'Should detect prototype pollution finding'
);
assert.equal(result.summary.prototypePollutionRisks, 1);
});
test('API Fuzz Auditor: Detects unhandled async exception in route handler (CWE-754)', () => {
const code = `
app.post('/api/transfer', async (req, res) => {
const result = await bankService.sendMoney(req.body);
res.json(result);
});
`;
const result = auditApiFuzzResilience({ routeCode: code });
assert.ok(
result.findings.some((f) => f.id === 'API-FUZZ-UNHANDLED-ASYNC-EXCEPTION'),
'Should detect unhandled async exception risk'
);
assert.equal(result.summary.unhandledExceptionRisks, 1);
});
test('API Fuzz Auditor: Detects unrestricted JSON body parser size (CWE-400)', () => {
const code = `
const app = express();
app.use(express.json());
`;
const result = auditApiFuzzResilience({ routeCode: code });
assert.ok(
result.findings.some((f) => f.id === 'API-FUZZ-BODY-PARSER-UNRESTRICTED-SIZE'),
'Should detect unrestricted body parser size'
);
assert.equal(result.summary.oversizedPayloadDosRisks, 1);
});
test('API Fuzz Auditor: Detects unbounded integer parsing (CWE-190)', () => {
const code = `
app.get('/api/items', (req, res) => {
const offset = parseInt(req.query.offset);
res.json({ offset });
});
`;
const result = auditApiFuzzResilience({ routeCode: code });
assert.ok(
result.findings.some((f) => f.id === 'API-FUZZ-UNBOUNDED-INTEGER-PARSE'),
'Should detect unbounded integer parse'
);
assert.equal(result.summary.typeConfusionRisks, 1);
});
test('API Fuzz Auditor: Fully hardened and validated API endpoint passes cleanly', () => {
const hardenedCode = `
import { z } from 'zod';
const UserSchema = z.object({
id: z.number().int().min(1).max(10000),
username: z.string().min(3).max(50)
}).strict();
app.post('/api/user', async (req, res, next) => {
try {
const validated = UserSchema.parse(req.body);
const safeTarget = JSON.parse(JSON.stringify(validated, (k, v) => (k === '__proto__' ? undefined : v)));
res.json({ user: safeTarget });
} catch (err) {
next(err);
}
});
`;
const result = auditApiFuzzResilience({ routeCode: hardenedCode });
assert.equal(result.complianceStatus, 'PASS');
assert.equal(result.totalFindings, 0);
});
test('Patch Engine: Injects Zod schema validation guard into vulnerable handler', () => {
const vulnerable = `
const { username, email } = req.body;
`;
const patched = synthesizeAndInjectPatch({ sourceCode: vulnerable, filename: 'routes/user.ts' });
assert.equal(patched.applied, true);
assert.ok(patched.patchedCode.includes("import { z } from 'zod';"));
assert.ok(patched.patchedCode.includes('BodySchema = z.object'));
assert.ok(patched.findingsResolved.includes('API-FUZZ-MISSING-SCHEMA-VALIDATION'));
});
test('Patch Engine: Injects safe prototype pollution guard before Object.assign', () => {
const vulnerable = `
const target = {};
Object.assign(target, req.body);
`;
const patched = synthesizeAndInjectPatch({ sourceCode: vulnerable, filename: 'routes/proto.ts' });
assert.equal(patched.applied, true);
assert.ok(patched.patchedCode.includes('const safeBody = JSON.parse'));
assert.ok(patched.patchedCode.includes('__proto__'));
assert.ok(patched.findingsResolved.includes('API-FUZZ-PROTOTYPE-POLLUTION'));
});
test('Patch Engine: Restricts body parser size limit to 100kb', () => {
const vulnerable = `
app.use(express.json());
`;
const patched = synthesizeAndInjectPatch({ sourceCode: vulnerable, filename: 'server.ts' });
assert.equal(patched.applied, true);
assert.ok(patched.patchedCode.includes("express.json({ limit: '100kb' })"));
assert.ok(patched.findingsResolved.includes('API-FUZZ-BODY-PARSER-UNRESTRICTED-SIZE'));
});
test('Canary Sandbox: MockFuzzHarness records route invocations and validates prototype security', async () => {
const code = `
fuzzHarness.testRoute(() => {}, { body: { title: 'safe_post', __proto__: { polluted: false } } });
`;
const result = await runInCanarySandbox(code);
assert.equal(result.success, true);
assert.equal(result.fuzzHarness.executedRoutes.length, 1);
assert.equal(result.fuzzHarness.executedRoutes[0].body.title, 'safe_post');
});
test('Canary Verification: Dual canary verifies API fuzzing patch mitigates vulnerability and preserves benign requests', async () => {
const original = `
const target = {};
Object.assign(target, req.body);
`;
const patched = `
const target = {};
const safeBody = JSON.parse(JSON.stringify(req.body, (k, v) => (k === '__proto__' ? undefined : v)));
Object.assign(target, safeBody);
`;
const canaryReport = await executeCanaryVerification({
originalCode: original,
patchedCode: patched,
vulnType: 'API_FUZZ_RESILIENCE',
attackPayload: '{"__proto__":{"polluted":true}}',
benignPayload: '{"name":"valid_user"}'
});
assert.equal(canaryReport.canaryStatus, 'VERIFIED_SECURE');
assert.equal(canaryReport.attackBlocked, true);
assert.equal(canaryReport.benignPreserved, true);
});
test('MCP Server: Registers vulnforge_fuzz_api tool with comprehensive schema', async () => {
const server = createVulnForgeServer();
const listHandler = server._requestHandlers.get('tools/list');
assert.ok(listHandler, 'ListTools handler must exist');
const result = await listHandler({ method: 'tools/list', params: {} });
const fuzzTool = result.tools.find((t) => t.name === 'vulnforge_fuzz_api');
assert.ok(fuzzTool, 'vulnforge_fuzz_api tool must be registered');
assert.ok(fuzzTool.inputSchema.properties.routeCode, 'routeCode property must be defined');
assert.ok(fuzzTool.inputSchema.properties.fuzzStrategy, 'fuzzStrategy property must be defined');
});
test('MCP Server: vulnforge_fuzz_api executes and returns valid JSON security report', async () => {
const server = createVulnForgeServer();
const callHandler = server._requestHandlers.get('tools/call');
assert.ok(callHandler, 'CallTool handler must exist');
const sampleRoute = `app.post('/api/item', (req, res) => { const { name } = req.body; res.json({ name }); });`;
const response = await callHandler(
{
method: 'tools/call',
params: {
name: 'vulnforge_fuzz_api',
arguments: { routeCode: sampleRoute }
}
},
{}
);
assert.ok(response.content && response.content[0].text);
const parsed = JSON.parse(response.content[0].text);
assert.equal(parsed.complianceStatus, 'FAIL');
assert.ok(parsed.totalFindings > 0);
assert.ok(parsed.summary.endpointsAudited >= 1);
});
test('MCP Server: vulnforge_full_scan processes fuzzRouteCode and attaches findings', async () => {
const server = createVulnForgeServer();
const callHandler = server._requestHandlers.get('tools/call');
assert.ok(callHandler, 'CallTool handler must exist');
const sampleRoute = `app.post('/api/order', (req, res) => { const { amount } = req.body; res.json({ amount }); });`;
const response = await callHandler(
{
method: 'tools/call',
params: {
name: 'vulnforge_full_scan',
arguments: { fuzzRouteCode: sampleRoute }
}
},
{}
);
assert.ok(response.content && response.content[0].text);
const summary = JSON.parse(response.content[0].text);
assert.ok(summary.totalFindings > 0);
assert.ok(summary.findings.some((f) => f.category === 'api_fuzzing'));
});