DAST scanner

Mass Assignment

If an API copies every field in the request body onto the record, a client can set fields it was never meant to control: a role, a price, a paid flag. The scanner adds one such field to a write request and reads the reply. If the field comes back with the value that was sent, the server took it.

DASTHighPrivilege escalationMass assignment

One field per request, every write endpoint

The scanner does not need to know what the endpoint expects. It picks every endpoint that writes, sends a body that contains only an escalation field, and checks whether the server echoes it back with the same value in a JSON response. A control request with a made-up field detects echoing endpoints; for those the scanner reads the written object back, keeps High only for fields that really persisted, and otherwise collapses the reflections into one Low note. HTML pages are never evidence.

Scanner session headers if logged in Endpoint inventory GET /api/items skipped POST /api/Users PUT /api/profile PATCH /rest/v1/profiles only POST, PUT and PATCH are tested 7 fields, one per request role "admin" is_admin true is_verified true email_verified true plan "enterprise" credits 99999 permissions "all" each body holds that field alone URL has supabase or rest/v1 role · is_admin · stripe_customer_id Request POST /api/Users {"role": "admin"} Prefer: return=representation nothing else in the body Response 201 Created {"data": { "role": "admin"}} the field is in the reply Finding · HIGH · confidence 0.85 status 200 to 299, and the field is anywhere in the body with a value equal to the one sent

1Pick the writes. Every POST, PUT and PATCH endpoint in the inventory is a candidate. GET endpoints are skipped. If the inventory holds no write endpoint the scanner stops without sending anything.

2One field at a time. Seven requests per endpoint, each with a JSON body holding only that field. The Prefer: return=representation header asks PostgREST-style APIs to send the row back. When the URL contains supabase or rest/v1 the list is replaced by three fields, not extended.

3Read the reply. The server answers. A non-2xx status ends the check for that field straight away, so a rejected request can never become a finding.

4The decision. The status is 200 to 299 and the field appears in the decoded body with a matching value. The finding is High with confidence 0.85, whatever the field, and it quotes the request and the status as evidence.

Finding the field when the API wraps its reply

Most APIs wrap the object they just created: {"status": "success", "data": {...}}, {"result": {...}}, or JSON:API's {"data": {"attributes": {...}}}. A check that only reads the top level calls every one of those clean. That is how a confirmed, reproducible mass assignment once reported as no finding.

Response body sent back { "status": "success", "data": { "user": { "id": 7, "role": "admin" "role": "customer" } } } level 0 · the root object keys: status, data role is not here. A top-level-only check stops now and calls the endpoint clean. role is not here, so keep descending level 1 · inside data keys: user role is not here, so keep descending level 2 · inside user keys: id, role role is here and "admin" equals the value sent: finding role is here but "customer" is not "admin": no finding How the value is compared sent true: the reply must hold true (or 1); the string "true" does not match anything else: str(reply) == str(sent), so 99999 matches "99999" search depth: 8 levels; arrays are searched item by item body is not JSON: the field name and the value both appear somewhere in the text

1Top level. The scanner sent {"role": "admin"}. The root object has status and data, no role. A top-level-only check would end here.

2Descend. Instead of guessing envelope names such as data or result, the scanner walks every value that is an object or a list. Next level: data holds user.

3Found, and the value matches. role is inside user and equals "admin". That is the evidence: the server stored or at least echoed what the client chose.

4The value is part of the test. If the same field came back as "customer", the server ignored the input and set its own default. Finding the key somewhere proves nothing, so this is not reported.

5Comparison rules. Booleans must come back as a boolean, other values are compared as strings so a number and its quoted form agree, and the walk stops at 8 levels. A body that is not JSON falls back to a text search for both the name and the value.

What it cannot reach yet: endpoints that validate first

The probe sends the escalation field on its own, with no valid base payload. An endpoint that checks its required fields rejects that request outright, so the escalation is never evaluated. The Juice Shop benchmark shows both sides of this.

POST /api/Users {"role": "admin"} the field alone, no other keys registerAdminChallenge Accepts a bare body nothing else is required to create the record 201 "role": "admin" reflected, value matches Finding HIGH · 0.85 POST /api/Feedbacks {"role": "admin"} the field alone, no other keys feedbackChallenge Validates first also demands a captcha required fields are missing Rejected status is not 2xx the check ends here Not evaluated reads as clean The gap is the missing base payload Reaching Feedbacks would need a valid body with the escalation field added to it. The scanner does not build one yet.

1A bare body is enough. POST /api/Users accepts a body that carries only the escalation field, so the server creates the record, echoes role: admin, and the scanner reports it. This is the Juice Shop registerAdminChallenge.

2Validation gets there first. POST /api/Feedbacks also demands a captcha and its own required fields. The same bare body is refused before any assignment happens, the status is not 2xx, and the scanner moves on.

3A real gap, stated openly. A clean result means "no bare-body escalation", not "no mass assignment". Endpoints with mandatory fields need a valid base payload that the probe does not yet supply.

Why it matters

Why it matters

If your API blindly accepts all fields from the request body, an attacker can promote themselves to admin with {"role": "admin"} or {"is_admin": true}, set {"price": 0.01} on an order update, change ownership with {"user_id": "other-user"}, or mark an order paid with {"status": "paid"} without paying.

Real-world breaches

GitHub (2012). Egor Homakov exploited a Rails mass assignment vulnerability to add his SSH key to the Rails core repository by manipulating the user_id attribute, gaining commit access to one of the most important open source projects. HackerOne (2015). A mass assignment flaw allowed adding yourself as a member of any bug bounty program by manipulating group membership parameters.

Vulnerable vs fixed code

The vulnerable handler writes whatever the client sent. The fix names the fields a client may set and leaves everything else out, so role and is_admin can only be changed by code that decides to.

// BAD: Accept all fields from request body
const body = await request.json()
await db.update("profiles", userId, body)  // Whatever the client sends gets written

Limits

What a finding means

The scanner checks reflection in the reply. It does not fetch the record afterwards to see whether the value was stored, and the probe itself is a real POST, PUT or PATCH that may create or change a record. The severity is always High at confidence 0.85, whatever the field.

SettingValue
Endpoints testedPOST, PUT, PATCH
Fields per endpoint7 (3 for Supabase-style URLs)
Success status200 to 299
Search depth in the reply8 levels
Authenticationsession headers added when the scan is logged in