HTTP Parameter Pollution: How Attackers Exploit Query String Parsing
Learn how HTTP Parameter Pollution works, how different frameworks parse duplicate parameters, how it bypasses WAFs, and how to mitigate HPP in your app.
HTTP Parameter Pollution: How Attackers Exploit Query String Parsing
HTTP Parameter Pollution (HPP) is a class of web attack that exploits inconsistencies in how servers, frameworks, and intermediaries parse query strings and POST bodies containing duplicate parameter names. The HTTP specification does not define what should happen when the same parameter appears more than once in a request — and as a result, different platforms make different choices. Attackers exploit these differences to inject unexpected values, bypass security controls, and manipulate application logic.
The Core Mechanics
Consider a request with a duplicate parameter:
GET /transfer?amount=100&to=alice&to=bob HTTP/1.1
There are two values for the to parameter. What does the server do with this? It depends entirely on the platform:
| Platform | Behavior | Result for to=alice&to=bob |
|---|---|---|
| PHP | Uses last value | bob |
| ASP.NET | Concatenates with comma | alice,bob |
| Node.js (Express/qs) | Returns array | ['alice', 'bob'] |
| Java (Servlet) | Returns first value | alice |
| Ruby on Rails | Uses last value | bob |
| Python (Django) | Returns last value | bob |
| Python (Flask/Werkzeug) | Returns first value | alice |
This divergence is the foundation of HPP attacks. When two different components in a system parse the same request differently, an attacker who understands both behaviors can construct a request that each component reads in a way that benefits the attack.
Attack Scenarios
WAF bypass
Web Application Firewalls inspect HTTP parameters for malicious payloads. If a WAF processes only the first occurrence of a parameter and the backend uses the last occurrence, an attacker can split a payload across two instances of the same parameter:
GET /search?q=safe_input&q=<script>alert(1)</script> HTTP/1.1
The WAF inspects q=safe_input (first value) and finds nothing malicious. The backend application reads q=<script>alert(1)</script> (last value) and processes the XSS payload.
The same technique works for SQL injection:
GET /item?id=1&id=1 UNION SELECT password FROM users-- HTTP/1.1
Logic manipulation
Consider a server-side OAuth flow where a redirect_uri parameter is validated:
POST /oauth/authorize?redirect_uri=https://trusted.com&redirect_uri=https://attacker.com
If the validation code reads the first value (https://trusted.com) but the redirect code uses the last value (https://attacker.com), the authorization code is sent to the attacker's domain. This was the mechanism behind several real-world OAuth HPP vulnerabilities.
Signature bypass
APIs that compute signatures over request parameters are vulnerable if the signing code and the processing code read different values from a duplicated parameter. The signature is computed over amount=1 but the transaction processes amount=1000.
Server-to-server HPP
HPP is not limited to client-to-server communication. When a server constructs URLs to forward to a backend API by appending user-supplied values, it may inadvertently create pollution:
# Vulnerable: user_id from request appended to backend URL
backend_url = f"https://internal-api/data?user_id={user_id}&format=json"
If user_id is 123&format=csv, the resulting URL becomes:
https://internal-api/data?user_id=123&format=csv&format=json
The backend may use csv instead of json, or the attacker can inject additional parameters into the forwarded request.
Framework-Specific Behavior in Detail
Understanding your framework's exact behavior is essential for reasoning about HPP risks.
Node.js with Express (qs library):
// qs (default query parser in Express) creates arrays for duplicates
// GET /search?tag=js&tag=security
req.query.tag // → ['js', 'security']
// Code that expects a string will fail unpredictably:
const upper = req.query.tag.toUpperCase(); // TypeError: req.query.tag.toUpperCase is not a function
If your application code calls string methods on a parameter without checking whether it's an array, an attacker supplying duplicate parameters can cause runtime errors or unexpected behavior.
ASP.NET:
// GET /page?id=1&id=2
// Request.QueryString["id"] returns "1,2" (comma-separated)
int.Parse(Request.QueryString["id"]) // FormatException — breaks integer parsing
PHP:
// GET /page?action=view&action=delete
// $_GET['action'] === 'delete' (last value wins)
If the access control check reads the first value (view) but the action handler reads the last value (delete), authorization is bypassed.
Mitigation Strategies
Explicit parameter extraction with strict type enforcement
The most reliable mitigation is to explicitly extract exactly one value per parameter and enforce the expected type, rejecting requests that contain duplicates:
import { Request, Response, NextFunction } from 'express';
function strictParam(name: string, req: Request): string {
const value = req.query[name];
if (Array.isArray(value)) {
throw new Error(`Duplicate parameter not allowed: ${name}`);
}
if (typeof value !== 'string') {
throw new Error(`Missing required parameter: ${name}`);
}
return value;
}
app.get('/transfer', (req: Request, res: Response) => {
try {
const to = strictParam('to', req);
const amount = strictParam('amount', req);
// process transfer...
} catch (err) {
res.status(400).json({ error: 'Invalid request parameters' });
}
});
Middleware to reject duplicate parameters
For broad protection, add middleware that rejects any request containing duplicate parameter names:
function rejectDuplicateParams(req: Request, res: Response, next: NextFunction) {
for (const [key, value] of Object.entries(req.query)) {
if (Array.isArray(value)) {
return res.status(400).json({
error: `Duplicate parameter '${key}' is not permitted`
});
}
}
next();
}
app.use(rejectDuplicateParams);
Consistent parsing across all components
When multiple systems process the same request — a WAF, an API gateway, and the application server — ensure they all use the same parameter resolution strategy. Document and standardize whether "first wins" or "last wins" applies at every layer. Inconsistency between layers is the root cause of most WAF bypass scenarios.
Avoid constructing URLs from user input
When forwarding parameters to backend services, extract each parameter value explicitly and reconstruct the URL from scratch using an URL-building library rather than string concatenation. Never append raw user input to a URL:
// Vulnerable
const backendUrl = `https://api.internal/data?user_id=${req.query.userId}&format=json`;
// Safe
const url = new URL('https://api.internal/data');
url.searchParams.set('user_id', strictParam('userId', req)); // explicit, single value
url.searchParams.set('format', 'json'); // hardcoded, not from user input
URLSearchParams.set() replaces any existing value for the key rather than appending. This ensures you always send exactly one value per parameter to downstream services.
Testing for HPP
Include HPP test cases in your security test suite. For each parameter in your application, verify:
- Requests with duplicate parameters are rejected or handled predictably.
- The value used for processing matches the value used for validation.
- URL construction for backend calls uses explicit parameter setting, not string concatenation.
Burp Suite's Param Miner extension and manual testing with duplicate parameters in Repeater are effective approaches for identifying HPP vulnerabilities in running applications.