Web Security

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.

March 9, 20266 min readShipSafer Team

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:

PlatformBehaviorResult for to=alice&to=bob
PHPUses last valuebob
ASP.NETConcatenates with commaalice,bob
Node.js (Express/qs)Returns array['alice', 'bob']
Java (Servlet)Returns first valuealice
Ruby on RailsUses last valuebob
Python (Django)Returns last valuebob
Python (Flask/Werkzeug)Returns first valuealice

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:

  1. Requests with duplicate parameters are rejected or handled predictably.
  2. The value used for processing matches the value used for validation.
  3. 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.

Check Your Security Score — Free

See exactly how your domain scores on DMARC, TLS, HTTP headers, and 25+ other automated security checks in under 60 seconds.