HTTP Parameter Pollution: Parsing Quirks, WAF Bypass, and Exploitation
HTTP Parameter Pollution (HPP) exploits a gap that the HTTP specification never closed: there is no rule for what a server should do when the same parameter name appears more than once in a single request. A query string like ?role=user&role=admin is perfectly valid HTTP, but every web stack resolves the duplication differently. Some take the first value, some take the last, some concatenate them, and some hand you an array. When a request passes through more than one component — a CDN, a WAF, a reverse proxy, and finally the application — those components often disagree about which value is "the" value. That disagreement is the entire attack surface.
HPP rarely makes headlines on its own, but it is a force multiplier. It bypasses input filters, smuggles payloads past WAFs by splitting them across duplicate keys, corrupts server-side request construction, and tampers with authorization decisions. This guide covers the parsing differences that make it work, practical exploitation against authorized targets, and how to shut it down. Everything below assumes you have written permission to test the systems involved.
How Servers Parse Duplicate Parameters
The first thing to internalize is that there is no "correct" behavior — only behavior. Before you can pollute anything, you need to know which value each layer of the stack will honor. The table of observed defaults across common frameworks looks roughly like this:
- PHP / Apache — last occurrence wins.
?id=1&id=2yieldsid=2. - ASP.NET / IIS — values are concatenated with a comma.
?id=1&id=2yieldsid=1,2. - Node.js (qs / Express) — duplicates become an array.
req.query.idis["1","2"]. - Python (Flask / Werkzeug) — first occurrence wins with
request.args.get(); full list viagetlist(). - Python (Django) — last occurrence wins with
QueryDict.get(). - Java (Servlet / Spring) — first occurrence wins with
getParameter(); full array viagetParameterValues(). - Go (net/http) — first occurrence wins with
r.FormValue(); full slice viar.Form["id"].
The exploit primitive emerges when two of these sit in the same pipeline. If an Apache-fronted WAF inspects only the first value but the PHP back-end reads the last, you can put a benign value first and your real payload last. The WAF clears it; the application executes it.
Confirming Parsing Behavior on a Target
Never assume defaults — applications override them, and middleware rewrites query strings. Fingerprint the actual behavior with a parameter you can observe in the response, typically one that gets reflected or echoed back:
GET /search?q=alpha&q=bravo HTTP/1.1
Host: target.example
Read the response. If it searches for alpha, the back-end is first-wins. If it searches for bravo, it is last-wins. If you see alpha,bravo, you are talking to an ASP.NET-style concatenator — which is its own goldmine for injection. Pasting the raw request into an HTTP request parser makes it easy to see exactly which duplicate keys and encodings you are sending before they hit the wire, so you are not debugging your own malformed test cases.
Do the same test in the body for application/x-www-form-urlencoded POSTs, because body parsers and query parsers in the same framework sometimes differ. And test mixed location pollution — the same key in both the query string and the body — since precedence between the two is yet another undefined behavior you can weaponize.
WAF Bypass via Split Payloads
The most reliable HPP technique against a WAF is payload splitting. A WAF that pattern-matches on a single parameter value sees each occurrence in isolation, so a signature that would fire on the whole payload never matches the fragments. On a concatenating back-end (classic ASP.NET), the fragments are rejoined into a working payload after inspection.
Consider a SQL injection signature that flags UNION SELECT. Split across duplicate keys, no single value contains the phrase:
GET /report?id=1/**/UNION&id=SELECT/**/1,2,3--+ HTTP/1.1
Host: target.example
On an ASP.NET stack this becomes id=1/**/UNION,SELECT/**/1,2,3--+. The comma sits inside an SQL context where it is harmless to the parser but lethal to the WAF signature. The same idea applies to XSS and command injection wherever the back-end rejoins values. When the back-end is last-wins rather than concatenating, the move is different: hide the malicious value as the last occurrence and feed the WAF an innocuous first occurrence.
# WAF inspects first value (clean), PHP back-end uses last value (payload)
GET /item?cat=electronics&cat=' OR '1'='1 HTTP/1.1
Host: target.example
Splitting pairs naturally with encoding tricks. If you also need to mangle the surviving fragment past a content filter, the WAF bypass transformer can generate case-variation, comment-insertion, and encoding permutations of each fragment, and the broader guide to bypassing a WAF walks through chaining HPP with chunked transfer and Unicode normalization.
Server-Side HPP and Outbound Request Tampering
Client-side and server-side HPP are different beasts. Client-side HPP injects extra parameters into links and forms the application generates, useful for crafting confusing CSRF or reflected payloads. Server-side HPP is more dangerous: it abuses the moment an application takes user input and reuses it to build an outbound request to an internal API or microservice.
Suppose a public endpoint forwards your request to an internal billing service and naively appends parameters:
POST /api/pay HTTP/1.1
Host: target.example
Content-Type: application/x-www-form-urlencoded
amount=100&account=12345
If the application builds the internal call as /internal/charge?account={account}&fee=2 by string concatenation, an attacker who sends account=12345%26fee=0 injects a second fee parameter into the internal URL. Depending on the internal service's precedence rules, the attacker-controlled fee=0 may override the trusted fee=2. This is the same root cause as SSRF parameter injection: untrusted input flows into a structured outbound request without re-encoding.
Authorization and Logic Bypass
HPP frequently breaks access control because authorization checks and business logic often read the parameter at different points in the request lifecycle. A common pattern: a routing or middleware layer reads the first user_id to authorize, while the data-access layer reads the last user_id to fetch the record.
GET /account?user_id=ME&user_id=VICTIM HTTP/1.1
Host: target.example
Cookie: session=...
If the auth filter is first-wins (Java/Spring's getParameter) and the ORM call somewhere downstream iterates the full value array and lands on the last element, the request authorizes as you but returns the victim's data — an IDOR delivered through parsing desync rather than a guessable identifier. The same split can flip boolean feature flags (?admin=false&admin=true), defeat anti-CSRF token comparison, or duplicate a coupon parameter to stack a discount the logic only meant to apply once.
A Practical Testing Workflow
Approach HPP methodically rather than spraying duplicates everywhere:
- Fingerprint first. Identify the back-end framework and confirm its precedence with a reflected parameter before crafting payloads.
- Map the layers. Note every component in the path — CDN, WAF, proxy, app — and test each boundary for parsing disagreement, not just the app.
- Test all input locations. Query string, body, and the same key spanning both. Don't forget JSON endpoints: many JSON parsers silently keep the last duplicate key, which is HPP in another wrapper.
- Pollute high-value parameters. Anything tied to authorization (
role,user_id,is_admin), pricing, redirects, or filters where a known WAF signature would normally fire. - Watch second-order sinks. Look for input that reappears in outbound URLs, logs, or generated links — that is where server-side HPP and client-side HPP live.
Defenses and Remediation
HPP is fundamentally a contract problem: components in a request path that fail to agree on how to interpret the same input. The fixes follow from that.
- Reject duplicates explicitly. The cleanest defense is to treat duplicate parameter names as a malformed request and return
400. If your framework supports strict query parsing, enable it; if not, add a small middleware that scans for repeated keys before any handler runs. - Read parameters by explicit index, never the convenience accessor. Decide whether your application means first, last, or all, and use the API that says so —
getParameterValues(),getlist(),r.Form[...]— instead of relying on the framework's silent default that the layer in front of you may not share. - Normalize at the edge. Have the WAF or gateway canonicalize the request — collapse duplicates to a single deterministic value — before inspection, so the WAF and the app see identical input. Inspecting only the first occurrence is the bug, not the mitigation.
- Re-encode when constructing outbound requests. Never concatenate user input into an internal URL or query string. Build outbound requests with a proper URL/query builder that URL-encodes values, which neutralizes injected
&and=delimiters and kills server-side HPP. - Validate against an allowlist. If
rolecan only be one of a fixed set, enforce that after parsing. Strong type and value validation turns an ambiguous duplicate into a rejected request. - Align the whole pipeline. Document and standardize parsing behavior across CDN, WAF, proxy, and application. Desync defenses are only as strong as the most permissive hop.
HPP rewards attention to detail precisely because it lives in the spaces between systems that each behave reasonably on their own. Confirm parsing behavior empirically, think in terms of layer boundaries rather than single endpoints, and remember that the same duplicate-key trick that bypasses a filter today may break an authorization check tomorrow. On the defensive side, the antidote is determinism: every component in the path must agree on exactly which value counts, and untrusted input must be re-encoded the moment it crosses into a new structured context.
Put this into practice
Generate and test these payloads interactively — free, in your browser.
Level up your security testing
Install the CLI
npx payload-playgroundExplore All Tools
Encoding, hashing, JWT & more
Browse Cheat Sheets
Quick-reference payload guides