
Both of these bugs come from the same blind spot: the application trusts the shape of the request more than it should. With HTTP Parameter Pollution, the trust failure is that no two layers in your stack agree on what to do when a parameter appears twice, so a value a WAF approves is not the value the backend acts on. With mass assignment, the failure is that a framework cheerfully binds every field in a JSON body to an object, including the fields you would never put on a form. Neither carries an injection signature, which is why both sail past scanners and straight into privilege escalation.
This post shows the concrete payloads, how different frameworks resolve duplicate parameters, how to confirm a mass-assignment elevation by re-reading the object, and the allowlist approach (DTOs, strong parameters) that fixes both. If your app runs Rails, Spring, Laravel, or any autobinding framework, these are bugs you should be probing in every API penetration testing scope.
HTTP Parameter Pollution is sending the same parameter name multiple times in one request and relying on the inconsistent way different components pick a value. Some frameworks take the first occurrence, some the last, some concatenate, and a WAF in front may evaluate a different one than the application behind it, which is what makes HPP useful for filter evasion.
For example, ?role=user&role=admin might be read as user by a validation layer and admin by the backend, or a WAF might inspect only the first value while the app uses the last. HPP shows up in query strings, POST bodies, and within a single field via array syntax like role[]=user&role[]=admin. It is a standard check in any thorough web application pentesting checklist.
The danger of HPP is that there is no standard, so each stack resolves duplicates its own way and a request that passes one layer means something else at the next. Knowing your target's behavior tells you which value to weaponize, and the resolution gap between a WAF and the app is the whole exploit.
# Request: color=red&color=blue
ASP.NET (classic) -> "red,blue" (concatenated)
PHP / Apache -> "blue" (last wins)
JSP / Tomcat -> "red" (first wins)
Node.js (express) -> ["red","blue"] (array)
Python (Flask) -> "red" (first via .get)A practical exploit: a payment WAF rule inspects the first amount parameter, but the PHP backend reads the last. You send the duplicate and the firewall validates a different value than the one that hits the charge logic. Always confirm the actual resolution against your specific target rather than trusting the table, because middleware and proxies can shift it.
Mass assignment, also called over-posting or autobinding, is when a framework automatically maps all incoming request fields onto an internal object, letting you set properties that should be off-limits. If a user-update endpoint binds the whole JSON body to a User model, adding a field the form never showed can flip privileges.
PATCH /api/users/me HTTP/1.1
Content-Type: application/json
{
"displayName": "Alex",
"isAdmin": true, <-- never exposed in the UI
"role": "superadmin", <-- bound anyway by the autobinder
"emailVerified": true,
"accountBalance": 999999
}Rails (update_attributes without strong parameters), Spring (@ModelAttribute binding the whole entity), and Laravel (unguarded $fillable) have all shipped this by default. Mass assignment is an OWASP API Security Top 10 entry, so it belongs in every API testing scope, and it overlaps directly with Broken Access Control.
Test mass assignment by discovering an object's full field set, submitting privileged fields the endpoint did not advertise, and checking whether they persist. The fastest way to learn the model is to read a GET response for the same object, because the read API often returns fields the write API silently accepts.
Capture a legitimate update in Burp Suite, add candidate fields (isAdmin, role, verified, balance, user_id, tenant_id), and resend. Re-fetch the object to confirm the change stuck. Try nested objects too ("owner":{"id":1}) and watch for cross-tenant assignment by setting an ID that belongs to someone else, which turns mass assignment into an IDOR. Probing this binding behavior across hundreds of endpoints is exactly the repetitive, stateful reasoning that agentic pentesting scales far better than a one-pass scan.
A confirmed mass assignment shows the privileged field persisting after the write, proven by re-reading the object, not just a 200 on the request. The flow is read, spot a field the form never exposes, set it on the write, then read again:
# 1. GET reveals the full model, including a role field
GET /api/users/me
-> {"id":91,"displayName":"Alex","role":"user","isAdmin":false}
# 2. PATCH adds the privileged field the UI never sends
PATCH /api/users/me
{"displayName":"Alex","role":"admin","isAdmin":true}
HTTP/1.1 200 OK
# 3. Re-read to confirm the elevation persisted
GET /api/users/me
-> {"id":91,"displayName":"Alex","role":"admin","isAdmin":true} <-- privilege escalation, persistedThe third response is the evidence: role changed to admin through a self-service endpoint. On a recent assessment of a fintech onboarding API, I proved impact by then hitting an admin-only route with the same session and getting a 200 where it previously returned 403. The persisted state change, plus the now-authorized admin action, is the finding, not the accepted PATCH on its own.
HPP turns into money when the validation layer and the charge layer read different copies of the same parameter. The confirmed finding is not the duplicate parameter, it is the receipt showing the lower charge. Here the WAF inspects the first amount and the backend honors the last:
POST /checkout HTTP/1.1
Content-Type: application/x-www-form-urlencoded
item=42&amount=1499.00&amount=1.00
# ^-- WAF sees 1499.00 (passes its fraud rule)
# ^-- PHP backend reads 1.00 (last wins)
HTTP/1.1 200 OK
{"order":"A-77213","charged":1.00,"status":"paid"} <-- charged 1.00 for a 1499.00 itemThe "charged":1.00 in the order receipt, matched against the gateway record, is the proof. The same disagreement powers filter evasion: a WAF that blocks role=admin on the first value never sees the second one the app actually binds. For deeper price-tamper patterns, see our writeup on business logic and payment tampering.
Fix both with explicit allowlists rather than trusting framework defaults. For mass assignment, bind only the fields you intend to accept: Rails strong parameters (params.require(:user).permit(:name, :email)), Spring DTOs instead of binding entities directly, Laravel $fillable with a tight list, and dedicated request/response schemas in your API layer. The cleanest design is a hard separation between the wire schema and the persistence model: accept a request DTO with only the fields a client may set, then map it explicitly to the entity, so a stray isAdmin has nowhere to land. Sensitive fields like role, balance, and tenant IDs should be settable only through dedicated, separately authorized endpoints.
For HPP, normalize duplicate parameters at the edge (pick first-wins or reject-on-duplicate and apply it consistently across WAF and app), validate that each parameter appears once where it should, and make sure your WAF and application resolve parameters the same way. Both flaws reduce to one principle from the OWASP Top 10: never let the client decide which internal properties get written, and never let two layers disagree about which value a request carries.