Body Param Fuzzer
Most real API bugs sit in the fields of a JSON body, not in the query string. While logged in, the crawler records the writes the app makes. The fuzzer replays each one many times, changing one field per request, and looks at what the server says back.
From a captured write to a fuzzed request
The fuzzer does not invent requests. It starts from writes the real app made, so the URL, method and the shape of the body are already valid. That is why a changed field reaches the code that handles it.
1Filter the capture. Only POST, PUT and PATCH requests that carry a body and returned 200, 201 or 204 are kept, and the body must parse as a JSON object. Each kept request is replayed with the logged-in user's token plus the original apikey header if it had one. The first 20 are fuzzed.
2Change one field at a time. For each of the first 10 keys the scanner rebuilds the body with that one value replaced and sends it to the same URL with the same method. Every other field stays valid so the request still reaches the handler.
3Count the requests. A string-valued key gets 12 payloads. Prototype-pollution keys are tried once per captured request, as an extra key rather than a replacement. That is at most 123 requests per captured write.
Four checks, four different rules
Each payload family has its own question about the response. They are deliberately simple: the scanner sends one fuzzed request and reads the status and the text.
1SQL injection. Three payloads go into one field. If the response text matches a database-specific error pattern that the recorded response did not already contain, the server leaked a database error, which means the value was placed in a query. This is the only Critical check. Generic crash strings (Traceback, TypeError, Internal Server Error) no longer count; a payload that only turns a success into a 5xx is a Medium "Possible SQL injection".
2XSS reflection. Two canary payloads. If the exact string comes back in the response, the field is echoed without encoding. It is High, not Critical, because where the echo ends up decides whether it runs.
3Type confusion. Seven wrong types replace a string-valued field. A 5xx means the handler did not guard the type. It is Medium: a crash, not yet an exploit.
4Prototype pollution. The three keys are added to the body. Node.js can merge them onto Object.prototype, which can bypass authorisation checks. The scanner reports High only when a follow-up clean request returns the unique marker (the pollution persisted); a server that merely answers below 400 gets a single Low "not confirmed" note per request.
What a finding proves, and what it does not
Two of the checks are looser than their names suggest. The scene shows where a finding means "the server accepted it" rather than "the server was exploited".
1A strict server is clean. A schema validator that rejects unknown keys answers 400, which is not below 400, so no finding is made.
2Read-back is the test. A server that quietly drops the key answers 200 and is no longer reported as High: only a Low "not confirmed" note remains, unless a clean follow-up request shows the marker.
3Only a persisted marker is High. The keys carry a unique marker value. A follow-up clean request that returns the marker proves the pollution outlived the request; a server that only answered below 400 gets a Low, confidence 0.30 note. The scanner still does not read Object.prototype itself, so confirm a High by checking that a property set on the prototype shows up elsewhere.
4Only database wording counts. Generic crash strings such as stack trace or Internal Server Error are no longer evidence, and a pattern already present in the recorded response is ignored. A payload that merely turns a successful request into a 5xx is a Medium "Possible SQL injection" at confidence 0.50. Check the response preview before acting on a finding.
Why it matters
Body parameter fuzzing catches what scanners that target URL parameters miss. Many APIs accept complex JSON bodies where individual fields are unsanitised. Prototype pollution can override security checks, bypass authorisation or reach remote code execution in Node.js. Sending null where a string is expected can crash the app or bypass validation. SQL and XSS payloads hidden in deeply nested JSON often get past a WAF.
Lodash CVE-2019-10744 (2019). Prototype pollution via defaultsDeep in Lodash, with more than 100 million downloads a month, enabled denial of service or remote code execution in Node.js apps. jQuery CVE-2019-11358 (2019). Prototype pollution in $.extend() affected about 74% of all websites at the time.
Vulnerable vs fixed code
The vulnerable handler merges whatever the client sent into a live object. The fix removes the dangerous keys before the body is used.
// BAD: the client's keys, including __proto__, are merged in
const body = await request.json()
Object.assign(settings, body)// GOOD: Strip dangerous keys before spreading
function sanitize(obj: Record<string, unknown>) {
const { __proto__, constructor, prototype, ...safe } = obj
return safe
}
const body = sanitize(await request.json())Limits
| Setting | Value |
|---|---|
| Requests fuzzed | first 20 successful JSON writes from the crawl |
| Keys per request | first 10 |
| Payloads per string key | 3 SQL injection, 2 XSS, 7 wrong types |
| Pollution keys per request | 3, added to the body |
| Needs | an authenticated crawl that captured JSON writes |
| Concurrency | 3 at a time, 0.3 s between requests |
Only the top-level keys of an object body are fuzzed; nested fields are replaced as a whole, not one by one. Bodies that are lists, form-encoded or multipart are skipped. The fuzzer replays real writes, so a successful probe can create or change records on the target.