HTTP Parameter Pollution in APIs: the WAF evaluates one value, your framework executes another
Your WAF flagged role=admin and blocked the request. Meanwhile, your Spring Boot controller processed role=user&role=admin and called getParameter("role"), which returns the first value: user. The WAF evaluated the second. The request went through. The WAF won. The attacker also won.
HTTP Parameter Pollution (HPP) does not exploit a classic injection vulnerability. It exploits the difference between what your WAF reads and what your framework executes. That difference is determined entirely by your framework's parsing strategy, documented by almost no API team and configured by default by almost no WAF vendor.
The architectural gap: WAF reads one value, framework executes another
The WAF inspects parameters individually, without context for how the framework resolves them. When a request arrives with id=1&id=2, the WAF evaluates both values in isolation and applies its rules to each separately. But the business logic receives only one. Which one depends entirely on the framework.
The Ethiack study found the most sophisticated HPP payload achieved a 70.6% WAF bypass rate. The simplest HPP payload achieved 17.6%. No WAF vendor blocked all three scenarios. That differential is not in payload sophistication. It is in the fact that the WAF inspects the value it believes will be used, while the framework uses a different one.
Behavior varies by framework in a fixed and documented way:
| Framework | Strategy | Read function |
|---|---|---|
| PHP (Apache) | Last value wins | $_GET['param'] |
| ASP.NET (IIS) | Comma-concatenation | Request["param"] |
| Express.js (qs) | Array | req.query.param[0] |
| Django | Last value wins | QueryDict.get() |
| Flask | First value wins | request.args.get() |
| Java Servlet / Spring | First value wins | getParameter() |
| Ruby on Rails | Last value wins | params[:param] |
An attacker who knows your framework positions the payload exactly where the WAF does not evaluate it. In PHP, the malicious parameter goes last. In Spring, it goes first. The WAF sees the safe value. The framework executes the attacker's value. ASP.NET complicates this further: it concatenates values with a comma, producing safe,<payload>. That can cause the payload to escape injection signatures that check the complete value.
This is not a WAF flaw that a signature update can fix. It is a structural consequence of the separation between the inspection layer and the execution layer.
API attack surface: query strings yes, JSON no
JSON request bodies do not carry the same duplicate-key ambiguity with the same execution semantics. RFC 8259 does not prohibit duplicate keys in JSON. Most parsers silently use the last value and do not expose the parsing differential that makes HPP exploitable. API gateways that validate JSON schema do not validate the same parameters when they arrive via query string.
The real HPP attack surface in APIs breaks down this way: query strings (HPP applies), POST with application/x-www-form-urlencoded (HPP applies). JSON body does not create the same differential. Most teams know their endpoints accept JSON but overlook query string parameters. Filters, sorting, scope, roles, pagination tokens, and resource identifiers frequently arrive via query string even in APIs that accept a JSON body.
CVE-2025-7783 (CVSS 9.4, Critical) exposed this directly in the Node.js ecosystem. The form-data library used Math.random() to generate multipart boundaries, producing predictable boundaries that enabled parameter injection into multipart requests. Affected versions: form-data < 2.5.4, 3.0.0-3.0.3, 4.0.0-4.0.3. The fix requires upgrading to 2.5.4+, 3.0.4+, or 4.0.4+. The impact is critical because form-data is one of the most downloaded packages on npm. It is present in file upload pipelines and API integrations that process form data at scale.
Account takeover via password reset parameter routing
H1 report #2341038, filed against Mars, documents the most direct impact: full account takeover via HPP in the password reset flow. The payload is email=victim@mail.com&email=attacker@mail.com. The server uses the first value to generate the reset token and the second to route the email. The attacker receives the victim's reset link at their own address.
This attack works because the business logic calls the parsing function at two different points in the code. Both points resolve the duplicate parameter differently. One extracts the first value (token generation), the other extracts the second (email routing). Neither checks for duplicates before acting.
CVE-2021-20085 (CVSS 8.8, High) in backbone-query-parameters 0.4.0 demonstrates the natural extension of the same parsing gap. The _setParamValue function handles query string parameters without proper sanitization, leading to prototype pollution via CWE-1321. The attack vector starts at HPP and ends at Object prototype contamination, with potential code execution depending on downstream object usage.
CVE-2022-25871 (CVSS 7.5, High) in querymen reproduces the same vector via a user-controlled query parameter handler. The critical detail: no fixed version of querymen is available. The vulnerability remains open across all published versions of the library, and the repository receives no active maintenance.
WAF bypass chains: the documented cases
H1 report #150083 documented XSS on IRCCloud's badges page delivered via parameter pollution. The payload was split across two values of the same parameter: neither one, in isolation, triggered the WAF's XSS signature. The framework reconstructed the full value and executed the script. This is the canonical HPP-as-payload-carrier model: the WAF sees fragments, the framework sees the complete attack.
H1 report #105953 affected the social sharing buttons on HackerOne's own blog. Duplicate URL parameters overrode the share destination, redirecting Facebook and Twitter shares to an attacker-controlled URL. The WAF did not flag the request because each occurrence of the URL parameter appeared valid in individual evaluation. The impact was client-side: a user clicking the share button distributed the attacker's URL, not HackerOne's.
H1 report #298265 used semicolon-based HPP to override the for parameter of the Greenhouse iframe on HackerOne's careers page. The iframe loaded external forms not controlled by HackerOne. Semicolons as parameter delimiters are handled differently across servers and frameworks: some interpret them as parameter separators, others as part of the value. This creates a second class of parsing gap that most WAFs do not cover in their default normalization rules.
Detection and defense: document your framework's parsing strategy
The first step is documenting how your framework resolves duplicate parameters. This is not a security task. It is an architecture task. Without that data, no WAF rule can be correctly configured for your specific stack.
The second is normalizing parameters before WAF inspection. API gateways should be configured with de-duplication policies that match the backend framework's resolution strategy. If the framework uses the first value, the gateway should reject or discard duplicates before the WAF evaluates them. If the framework uses the last, the policy must ensure the WAF evaluates the same value the framework will execute.
The third is including duplicate parameters in every penetration test. OWASP WSTG defines the methodology: send multiple occurrences of the same parameter and observe which value the application uses. Then compare that against the value the WAF log records. No generic DAST tool covers this by default across all endpoints.
The MAGO team tool (mago.team) automatically tests duplicate parameters across all API endpoints, identifying which framework and WAF combinations create the evaluation differential.
The framework behavior matrix is the only configuration fact that makes HPP exploitable or not in your specific stack. Document it. Configure your WAF against it. Test both sides. The 70.6% bypass rate is not a WAF failure. It is the consequence of assuming a generic security layer understands your application's parsing semantics.
Top comments (0)