A request that carries the same key twice lets the WAF read one copy and the application act on the other - a three-step differential probe confirms which value the framework resolves and whether a security gate can be bypassed.
HTTP parameter pollution lands when a request carries the same key twice and the component that checks it disagrees with the component that acts on it. A WAF that reads the first occurrence waves through the payload in the second; an application that validates the first value passes the second to the business layer unchecked. Neither component is broken in isolation - the vulnerability lives in the gap between their conflicting resolution rules.
No HTTP specification mandates how a server must handle duplicate query or body parameters. Each framework picked its own rule, which means an attacker controls where the winning value lands by choosing how many copies to send and in what order. The table below covers the six runtimes that appear most often in production stacks - and every row resolves differently.
The comma-join behaviour in ASP.NET is the hardest to handle: it turns a simple string into an unexpected multi-value format that may break a downstream parser in a different way from the original injection, sometimes triggering a secondary vulnerability rather than the primary one.
WAF bypass works when the WAF reads the first parameter and the application reads the last. Inject the benign value first and the payload second. The WAF sees the benign value and allows the request; the application acts on the payload. Input-validation bypass is the inverse: the application validates the first value but passes the second to the business layer without re-checking it. Both shapes produce the same confirming artifact - a server action that reflects the marker value, not the checked one.
# Baseline: single parameter, server validates and executes normally GET /transfer?to=alice&amount=100 HTTP/1.1 Host: bank.example Authorization: Bearer eyJ... # Last-wins probe: safe value first, marker second GET /transfer?to=alice&amount=100&amount=62615533 HTTP/1.1 Host: bank.example Authorization: Bearer eyJ... # If receipt shows 62615533 -> last-wins confirmed # First-wins probe: marker first, safe value second GET /transfer?to=alice&amount=62615533&amount=100 HTTP/1.1 # If receipt shows 62615533 -> first-wins confirmed
The nonce 62615533 is recognizable in audit logs without causing real harm - confirming on a staging environment avoids any side effect entirely. The probe reads no stored data and writes no new record; the confirming artifact is which value the server reflected in the receipt.
A single duplicate-key request is not enough evidence on its own. Some frameworks resolve both values as equal when the input happens to be identical, and a single probe cannot distinguish that from a true last-wins runtime. Three requests per parameter close the argument: a baseline with one value, a last-wins probe, and a first-wins probe. Only one of the two probe orders should diverge from the baseline - that order names the resolution rule the runtime applies.
| Parameter type | HPP outcome if attacker value wins | Severity |
|---|---|---|
| Amount or quantity | Arbitrary substitution in business logic | High |
| User ID or account ref | Horizontal access control bypass (BOLA path) | Critical |
| Role or privilege flag | Vertical privilege escalation | Critical |
| Redirect URL | Open redirect to phishing host | Medium |
| HMAC signature input | Signed value differs from executed value | High |
Automated DAST discovers HPP by injecting duplicate keys into every query and body parameter position and watching for a behavioral divergence against the baseline. The probe must vary order because first-wins and last-wins are both real runtime configurations. The confirming signal comes from the server's action, not from a reflected string - many JSON APIs do not echo parameters back into the body, so the scanner has to track business outcomes: status transitions, receipt fields, audit-log side effects observable without triggering a real transaction.
Remediation is always at the parsing layer. Reject requests that supply the same key more than once, or explicitly document which copy the application takes and configure every WAF rule and every validator to the same semantics. A one-line framework rule that converts duplicates into a 400 Bad Request before the handler runs is the only reliable fix; relying on the WAF to strip duplicates upstream trades a parsing bug for a WAF-bypass surface when the WAF and the backend interpret the key-strip differently.
The vulnerability is a proven behavioral gap, not a theoretical one. A differential that confirms the winning value across both probe orders closes the severity argument before the ticket reaches a developer - and the fix is a single parser-layer rejection rule, not a WAF pattern that the next framework update can silently undo.