HTTP parameter pollution is what happens when you send the same parameter name twice and the layers handling your request quietly disagree about which copy counts. There is no rule in the HTTP specification that says what a server should do with ?role=user&role=admin. Some stacks keep the first value, some keep the last, some glue them together, some hand the application a list. When a firewall, a proxy, a framework, and a backend service each answer that question differently, a security check can run against one value while the real action runs against another.
Why duplicate parameters have no single answer
Because nobody ever standardised one. The query string is a convention, not a typed format, and every language grew its own habit. Ask four runtimes what name equals in ?name=a&name=b and you get four defensible answers.
- First occurrence wins. The parser stores the first value it sees and ignores later copies. You read
a. - Last occurrence wins. Later copies overwrite earlier ones. You read
b. - Concatenation. The values are joined, sometimes with a comma, sometimes with a separator that depends on the platform. You read something like
a,b. - Array. The parser builds a list and hands you
["a","b"]. Code that expected a string then does whatever a string operation does to a list, which is rarely what the author had in mind.
None of those is wrong on its own. The bug appears when two of them sit in the same request path. The same rules apply to a form encoded body, to JSON bodies with duplicate keys, and to multipart form fields. Anywhere a name can appear twice, somebody has to pick, and the pickers do not consult each other.
A security control only protects the value it actually read. If the business logic reads a different copy of the same parameter, the control was never in the request path at all.
Server side pollution: the check and the action read different values
The classic case is a filter or an authorization check placed in front of an application that parses the request its own way. Take an invented app, Acme Billing, with a transfer endpoint. A gateway inspects incoming requests and refuses any transfer where the source account does not belong to the caller. The gateway is written on a stack that takes the first occurrence of a parameter. The application behind it runs on a stack that takes the last.
POST /api/transfer HTTP/1.1 Host: acme-billing.example Content-Type: application/x-www-form-urlencoded from=ACC-1001&to=ACC-9000&amount=25&from=ACC-7777
The gateway parses from as ACC-1001, the caller’s own account, and approves the request. The application parses from as ACC-7777, someone else’s account, and moves the money. Both components behaved exactly as documented. The request passed a check that examined a value the transfer never used.
The same shape shows up around roles and flags. If an admin console accepts role from a form and a validation layer only inspects the copy it happens to read first, a second role=admin further down the body can reach the code that writes the record.
Why it defeats pattern matching filters
Splitting a value across duplicates also breaks filters that look for a payload in one place. A filter scanning each parameter value in isolation sees two short, unremarkable fragments. A backend that concatenates them sees one joined string. Nothing was encoded or obfuscated. The payload was simply distributed across copies that the filter judged separately and the application joined together. That is the same class of failure as HTTP request smuggling, our sibling post on parser disagreement, where a front end and a back end split one byte stream into a different number of requests. Different unit, identical root cause: two parsers, one input, two readings.
Client side pollution: the parameter that lands in a generated link
Client side pollution is the version where your extra parameter is reflected into a URL the page builds, rather than into a decision the server makes. Acme Billing renders a share link by copying the current invoice value into a template:
/invoice/view?invoice=INV-42%26mode%3Dprint
renders href="/invoice/export?invoice=INV-42&mode=print&format=pdf"
Because the encoded ampersand was decoded and pasted straight into the new URL, the attacker added a parameter to a link the application generated. The interesting targets are the parameters that steer behaviour: a redirect or next value, a format switch, a callback host, a token scope. Get an unexpected copy of one of those into a link and the destination the user clicks is no longer the destination the developer wrote. Where the polluted parameter controls where the browser goes next, the outcome looks like an open redirect, reached by an injected duplicate rather than by editing the parameter the page expected.
The same thing happens on the server when an application forwards a request onward. A service that rebuilds a downstream call by pasting user values into a query string can be made to add a parameter to that internal call, which is how a harmless looking field ends up setting an internal flag no external caller was ever meant to touch.
How do you prevent HTTP parameter pollution?
Every fix here is one idea in different clothing: make sure there is only ever one answer, and make sure every layer gets that same answer.
- Reject duplicates outright. If your API never legitimately accepts a repeated name, treat a second occurrence as a malformed request and return a 400. This is the cheapest fix and it removes the ambiguity instead of managing it. Allow repetition only for fields that are genuinely lists, and declare those explicitly.
- Normalise before any security decision. Canonicalise the request at the edge, collapsing or rejecting duplicates, so that everything downstream reads an input that can only be read one way. A check that runs on raw, unnormalised input is guessing.
- Parse once, pass a typed object forward. The most durable structural fix. Decode the request a single time into a validated object with declared types, then hand that object to the gateway logic, the business logic, and the outbound call. Reparsing the raw query at each hop is what creates the gap.
- Never let a filter and the application disagree about parsing. If a gateway sits in front of your app, test them against the same duplicated inputs and confirm they resolve to the same value. If they cannot be made to agree, the gateway should refuse ambiguous requests rather than interpret them.
- Enforce a schema. A declared schema that names each field, its type, and its cardinality turns a duplicate into a validation error before any handler sees it.
- Build outbound URLs with a real encoder. When user input goes into a link or a downstream call, use a URL builder that encodes each value, so an ampersand stays data and never becomes a separator. Never build a query string by string concatenation.
- Do not put authorization in the filter. Ownership and permission checks belong next to the code that performs the action, reading the same variable that code uses. Distance between the check and the action is the space this bug lives in.
You can find related teardowns under injection and input.
Why does this survive code review?
Nothing in the code looks wrong. Each layer reads a parameter, and each one is correct by its own documentation. The flaw only exists in the seam between two components that nobody wrote together, and it takes a request that no test suite generates: a well formed request that simply says the same thing twice. Finding it means questioning an assumption that never got written down, that every layer sees the same request. That is the kind of assumption an autonomous security researcher that reasons about how an application is meant to work, rather than replaying a fixed payload list, is built to test. You can read more about that approach on our about page.
Frequently asked questions
What is HTTP parameter pollution?
It is sending the same parameter name more than once in a query string or body and exploiting the fact that different layers disagree about which copy wins. One layer may read the first value, another the last, so a check and the action it guards can end up using different data.
What is the difference between server side and client side parameter pollution?
Server side pollution targets a decision on the server, where a gateway or filter reads one copy of a parameter and the application logic reads another. Client side pollution targets a URL the page or service builds, where an injected extra parameter changes a generated link, redirect, or downstream call.
Why does the same request give different values on different stacks?
The HTTP specification never defined what to do with duplicate parameter names. Some parsers keep the first occurrence, some keep the last, some join the values together, and some build an array, so identical bytes produce different results on different platforms.
How do you prevent HTTP parameter pollution?
Reject duplicate parameters outright unless a field is genuinely a list, normalise the request before any security decision, parse it once into a typed object that every layer shares, and confirm that a gateway and the application resolve duplicated inputs to the same value.
Put an autonomous researcher on your own systems
UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.
