DAST scanner · special

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.

DASTMediumHighCriticalInjection risk

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.

Captured by the authenticated crawl then the first 20 kept requests are fuzzed POST /api/orders 201 kept: a write, a success, a JSON object body GET /api/orders 200 dropped: not POST, PUT or PATCH POST /api/login 401 dropped: status is not 200, 201 or 204 PATCH /api/cart 200 dropped: the body is a list, not an object POST /api/orders · captured body, one field changed per request {"item": "' OR '1'='1' --", "qty": "2", "note": "gift"} key 1 of 3: SQL injection payload {"item": "mug", "qty": "<script>alert(1)</script>", "note": "gift"} key 2 of 3: XSS payload {"item": "mug", "qty": "2", "note": "gift", "__proto__": {"polluted": "true"}} added to the body, not swapped in per key: 3 SQL injection + 2 XSS + 7 wrong types (string-valued keys only) = 12 per request: up to 10 keys x 12 + 3 pollution keys = 123 requests

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.

SQL injection · 3 payloads ' OR '1'='1' -- 1'; SELECT-- the response text matches a database-specific error pattern SQL syntax · SQLSTATE · ORA-nnnnn · syntax error at or near … third payload: ' UNION SELECT NULL,NULL-- CRITICAL · 0.90 XSS reflection · 2 payloads <script>alert(1)</script> "><img src=x onerror=alert(1)> the payload appears verbatim in the response a plain substring test on the response text the content type and the place it appears are not examined HIGH · 0.85 Type confusion · 7 values 99999 · true · [] · null -1 · 0 · 999999999999 the status is 500 or higher only for keys whose original value is a string the server crashed on a type it did not expect MEDIUM · 0.70 Prototype pollution · 3 keys __proto__: {"polluted": "true"} constructor.prototype · "__proto__.polluted" the status is below 400 the key was accepted instead of refused nothing else in the response is checked HIGH · 0.90

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".

Strict schema refuses keys it does not know + "__proto__": {…} 400 Bad Request No finding status is not below 400 Ignores unknown keys drops the key, handles the rest + "__proto__": {…} 200 OK Finding · HIGH Object.prototype is unchanged Merges keys into an object copies every key it receives + "__proto__": {…} 200 OK Finding · HIGH Object.prototype is polluted 13 error patterns behind the SQL injection rule database wording SQL syntax pg_query ORA-\d{5} SQLITE_ERROR syntax error unterminated SQLSTATE no longer counted stack trace Traceback TypeError ValueError Internal Server Error NullPointerException one regex pass over the response text, compared with the recorded response; generic runtime errors are no longer counted

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

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.

Real-world breaches

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)

Limits

SettingValue
Requests fuzzedfirst 20 successful JSON writes from the crawl
Keys per requestfirst 10
Payloads per string key3 SQL injection, 2 XSS, 7 wrong types
Pollution keys per request3, added to the body
Needsan authenticated crawl that captured JSON writes
Concurrency3 at a time, 0.3 s between requests
What it stays quiet about

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.