HTTP Parameter Pollution
HTTP Parameter Pollution (HPP) exploits differences in how web servers, application frameworks, and WAFs handle duplicate HTTP parameters. By supplying the same parameter multiple times, attackers can bypass security controls, manipulate application logic, and evade WAF detection.
Warning
Frameworks: WSTG-INPV-04 · ASVS 5.0 V15.3.7 · CWE-235 · T1190
Why Duplicate Parameters Are Legal
A repeated key is not malformed. Neither the URI syntax nor the HTML form encoding makes
?a=1&a=2 an error, so every layer is free to choose: first, last, an array, or a
comma-joined string. Each choice is defensible alone and a defect in combination, because a request
crosses a WAF, a proxy, a router and a handler, and two of them may not agree.
So the precondition is two parsers, not one. Where the WAF and the origin agree, a duplicate is only a duplicate. HPP matters in three places: a control reads a different occurrence than the handler, the handler concatenates duplicates into a string it later parses, or the application copies user input into a URL it builds server-side.
How Frameworks Handle Duplicate Parameters
PHP/Apache: Uses LAST occurrence — ?id=1&id=2 → id=2
ASP.NET/IIS: Concatenates with comma — ?id=1&id=2 → id=1,2
Python (Flask): Uses FIRST occurrence — ?id=1&id=2 → id=1
Python (Django): Uses LAST occurrence — ?id=1&id=2 → id=2
Node.js (Express): Creates array — ?id=1&id=2 → id=[1,2]
Java (Tomcat): Uses FIRST occurrence — ?id=1&id=2 → id=1
Ruby (Rails): Uses LAST occurrence — ?id=1&id=2 → id=2
Go (net/http): Uses FIRST occurrence — ?id=1&id=2 → id=1
Tip
Do not infer the rule from the table — confirm it per target. Send the same parameter twice with
distinguishable values and look for the winner in the response, the filter or the error:
?q=alpha&q=bravo. For a numeric field use one in-range and one out-of-range value, so
you see which the business rule enforced. JSON bodies behave the same way: most parsers silently keep
the last duplicate key, so {"role":"user","role":"admin"} is worth one request wherever a
proxy inspects the body.
Server-Side and Client-Side Are Different Bugs
Client-side HPP is a data-flow question inside one page: the code that reads location.search
and the code that acts on it may take different occurrences. URLSearchParams.get() returns
the first; a loop that overwrites on each & returns the last. Nothing reaches the
server, so the impact is whatever the DOM sink does with the value.
WAF Bypass via HPP
For the WAF test the positive is a differential, not a reflection: the benign-valued duplicate returns the normal page, and the malicious-valued duplicate returns the database error or the boolean shift. If both are blocked, the WAF reads the same occurrence as the application — record that instead of calling it a bypass.
# Scenario: WAF blocks SQL injection in the 'id' parameter
# WAF checks FIRST parameter, PHP app uses LAST
# Blocked by WAF:
GET /page?id=1' OR 1=1--
# Bypass with HPP:
GET /page?id=1&id=1' OR 1=1--
# WAF sees id=1 (safe) → passes
# PHP sees id=1' OR 1=1-- (malicious) → SQL injection
# Another example with POST:
POST /transfer
Content-Type: application/x-www-form-urlencoded
amount=100&to=attacker&amount=999999
# WAF validates amount=100 (under limit)
# App processes amount=999999 (over limit)
# ASP.NET concatenation abuse:
GET /search?q=safe&q=' OR 1=1--
# ASP.NET sees: q=safe,' OR 1=1--
# The concatenated value bypasses simple pattern matchingBusiness Logic Bypass
Logic tests need a state change you can observe: a balance, an order total, a role on the next request. A duplicate that is accepted and ignored looks identical in the response, so re-read the resource afterwards, and use amounts or identifiers that will not collide with another tester's data.
# Override authorization checks:
POST /transfer
from_account=12345&to_account=67890&from_account=99999
# If app uses last value: transfers from account 99999
# even though the user only owns account 12345
# Price manipulation:
POST /checkout
price=100&item=widget&price=1
# App charges $1 instead of $100
# Access control bypass:
GET /api/users?role=user&role=admin
# If the app creates an array: role=[user, admin]
# Authorization check may see 'user' but grant 'admin' access
# Vote/rating manipulation:
POST /vote
post_id=123&vote=1&vote=1&vote=1&vote=1&vote=1
# Some apps count each parameter as a separate voteServer-Side HPP
This is the variant with real reach. A handler assembles a downstream request by concatenating user
input, and an & or # in the value becomes a delimiter in the new URL,
injecting a parameter the developer never wrote. The payment and share-link examples below are the same
bug at different stakes; both are confirmed by reading the downstream request, not the first response.
# When the application constructs URLs server-side using user input:
# Application code (vulnerable):
# url = "https://api.payment.com/charge?" +
# "amount=" + user_amount +
# "&merchant=trusted_merchant"
# Attack: Include & in the amount parameter:
# user_amount = "1&merchant=attacker_merchant"
# Resulting URL:
# https://api.payment.com/charge?amount=1&merchant=attacker_merchant&merchant=trusted_merchant
# If the payment API uses the FIRST merchant parameter,
# the payment goes to the attacker
# Social media sharing HPP:
# Application builds share URL:
# https://social.com/share?url=USER_INPUT&title=Site+Title
#
# Attack: url=attacker.com&title=Click+Me&url=attacker.com
# Overrides the title with attacker-controlled contentTesting Methodology
- 1. Identify the backend technology (PHP, ASP.NET, Flask, Express, etc.)
- 2. Test duplicate parameters — which value does the app use? (first, last, array, concatenated)
- 3. Test WAF bypass by splitting injection payloads across duplicate params
- 4. Test business logic with conflicting duplicate parameter values
- 5. Test server-side HPP by including & in parameter values
- 6. Test both GET and POST parameters (may behave differently)
- 7. Test JSON body with duplicate keys:
{"id":1,"id":2}
Confirming a Finding
A bypass is proven when a later request shows the effect — a filtered list, a changed role, a transfer that moved the wrong amount. Capture the single-value and duplicate-value requests side by side so the client can see which parser differed, then re-read the object the action touched. A response that differs only in length is a candidate; a state that differs is the finding.
# 1. Which occurrence wins? Send distinguishable values.
curl -s 'https://target.com/search?q=alpha&q=bravo' | grep -o 'alpha\|bravo' | sort -u
# 2. Control vs candidate, measured the same way
curl -s -o /dev/null -w 'single %{http_code} %{size_download}\n' \
'https://target.com/item?id=1'
curl -s -o /dev/null -w 'dup %{http_code} %{size_download}\n' \
"https://target.com/item?id=1&id=1%27%20OR%201%3D1--"
# 3. JSON duplicate keys — most parsers keep the last silently
curl -s -X POST https://target.com/api/user \
-H 'Content-Type: application/json' \
-d '{"role":"user","role":"admin"}'Evidence Collection
Request: HTTP request with duplicate parameters
Response Comparison: Show different behavior with single vs duplicate params
Parser Rule: Which occurrence each layer used — first, last, array or join — with the two distinguishable values that proved it
State Change: The resource re-read after the action, showing the effect rather than only a different response length
Downstream URL: For server-side HPP, the URL the application constructed, captured from a proxy or the receiving service
CVSS Range: WAF bypass: depends on underlying vuln | Logic bypass: 5.3–8.6
Remediation
- • Reject duplicate parameters: If the application expects a single value, reject requests with duplicates.
- • Consistent parsing: Ensure WAF and application use the same parameter parsing logic.
- • Validate on the server: Validate and sanitize all parameters after the framework parses them.
- • URL-encode user input: When constructing URLs server-side, URL-encode all user values to prevent & injection.
- • Build downstream URLs with a builder: Use a parameter API (for example
URLandsearchParamsin Node, orurlencodein Python) instead of concatenating strings, so&in a value stays data. - • Normalise at one layer: If a CDN or gateway can reject duplicate keys for scalar parameters, do it there rather than in each handler — but keep the application check, since the gateway is bypassable.
False Positive Identification
- Duplicate param handling by design: Some applications intentionally accept arrays via duplicate parameters (e.g., ?tag=js&tag=python) — verify the duplicate changes behavior maliciously, not just handled normally.
- Server vs. framework behavior: Different servers handle duplicate params differently (first wins vs. last wins vs. array) — check the specific tech stack's documented behavior.
- WAF bypass without backend impact: Successfully bypassing the WAF via HPP is only relevant if the backend actually processes the polluted parameter in a harmful way.
- Duplicates that carry the same value: Sending the same value twice tests nothing. The finding requires distinguishable values, because only then can you see which occurrence each layer read.
- A cache that canonicalises the query: Some CDNs sort or de-duplicate query parameters before forwarding. That removes the ambiguity for every backend behind them — check by varying parameter order and watching the cache key.
Parameter Pollution Practice
Exploit server-side parameter pollution where user input reaches internal APIs.