A brief look at HTTP Parameter Pollution
I’d like to take some time to dive into how some common backend languages handle HTTP Parameter Pollution.
But first, what is parameter pollution? OWASP gives us a really complete definition:
HTTP Parameter Pollution tests the applications response to receiving multiple HTTP parameters with the same name; for example, if the parameter username is included in the GET or POST parameters twice.
Supplying multiple HTTP parameters with the same name may cause an application to interpret values in unanticipated ways. By exploiting these effects, an attacker may be able to bypass input validation, trigger application errors or modify internal variables values. As HTTP Parameter Pollution (in short HPP) affects a building block of all web technologies, server and client-side attacks exist.
There’s a lot of ways to exploit this type of bug, ranging from simply duplicating HTTP parameters to injecting encoded delimiters to trick applications into adding them in server-side requests.
While these lasts ones can be more interesting and impactful, the main focus of this article will be in how different backend languages handle receiving multiple instances of the same HTTP Parameter and how that may be exploited. We’ll leave the advanced injection stuff for a later date.
Go
Let’s start with the language I’m most familiar with, Go. We’ll be looking at the standard “net/http” library.
code used for this test
package main
import (
"fmt"
"net/http"
"strings"
)
func parseQuery(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
fmt.Printf("Query variable is: %s\n", query)
test, found := query["test"]
if !found || len(test) == 0 {
fmt.Println("No query params passed")
return
}
w.WriteHeader(200)
w.Write([]byte(strings.Join(test, ",")))
}
func main() {
queryHandler := http.HandlerFunc(parseQuery)
http.Handle("/", queryHandler)
http.ListenAndServe(":1337", nil)
}Requests are represented using the http.Request struct, this struct contains URL field with a helpful .Query() method to return the query values as a url.Values object, which is simply a map with the following format map[string][]string
Since this function already returns a slice for each QueryParam, passing multiple QueryParams will just add more items to that slice. Testing it out we can see this is the case:
curl -g http://localhost:1337/?test=leet
curl -g http://localhost:1337/?test=leet&test=haxor

There’s also a helpful Query().Get() method which returns the first value associated with a given key, ignoring all values afterwards.
Looking for examples online, I came across this post by Rick Ramgattie which outlines a parameter pollution vulnerability in VMWare’s Concourse CI CVE-2022–31683 .
The vulnerability stems from another function that can parse QueryParams r.FormValue, this function is meant to parse HTTP form data coming from a POST or PUT request. However if no such data is present it then tries to get data from the QueryString parameters.
The full priority is outlined in the documentation :
FormValue returns the first value for the named component of the query. The precedence order:
1. application/x-www-form-urlencoded form body (POST, PUT, PATCH only)
2. query parameters (always)
3. multipart/form-data form body (always)
Taking all of this into account, I can see 2 possible sinks for parameter pollution vulnerabilities:
- Code that uses
URL.Query().Get()andrequest.FormValue()in different validation steps, as these can contain different values.
For example:
POST /api/deletePost?postId=1
Host: vulnerable-site.com
Cookie: auth=dG91Y2gtZ3Jhc3MK==
Content-Lenght: 8
postId=2
This would be vulnerable if URL.Query() is used to check Authorization for a given post, but the value to perform the delete is taken from`request.FormValue
- Code that uses
URL.Query().Get()for one step, andURL.Query()in another.
Example:
POST /api/sharePost?postId=1&postId=2&postId=3
Host: vulnerable-site.com
Cookie: auth=c3RvcC1yZWFkaW5nLXRoaXMK
Similarly to the one above, this code would be vulnerable if the Authorization check is performed using URL.Query().Get() and then the share function gets multiple values using URL.Query(). It’s important to note that we’re assuming this hypothetical share function supports getting multiple parameters and won’t error out.
ExpressJS
Moving on, let’s look at how JavaScript applications fair. There’s a gazillion options to pick but I’m going with Express running on Node for this experiment. Feel free to tell me about the ultra-hardened / super-broken framework of your choosing at @polarizeflow on Twitter.
code used for this test
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => {
console.log("Value of complete query object: ");
console.log(req.query);
console.log("Value of test property: ");
console.log(req.query.test);
res.send(req.query)
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})ExpressJS exposes the query object for any given requests, which is an object with all values parsed from the query. We can then call any specific query value by accessing it as a property of said object:
curl http://localhost:3000/?test=asd
curl http://localhost:3000/?test=asd&test=bacon

As shown above, the value for a given parameter is returned as an array when there’s more than one instance. There is a req.body that works for POST data parameters but unlike the Go example it does not grab data from the querystring when no POST data is sent.
We can also get Express to parse our parameters as an object by using bracket notation:
curl -g http://localhost:3000/?test[inside]=test

This results in some weird behavior when sending multiples of the same parameter:
curl -g http://localhost:3000/?test[inside]=true&test=where

Notice how the where key was set to the boolean true instead of the string true. Weird!
In the end, we can manipulate the query object in 3 different ways:
- Sending one parameter ->
?test=test
The “intended” way, this will result in {test: 'test'}, with calls to req.query.test returning a simple test string.
- Sending multiple copies of the same parameter ->
?test=test&test=zest
Getting better, this will result in {test: ['test', 'zest']}, with calls to req.query.test returning the array. Could be exploited similarly to our Go ‘share’ example.
- Sending parameters using bracket notation ->
/?test[inside]=true&test=where
Now it’s just weird, this payload will result in {test: {inside: 'true', where: true}}, in which test is now an object and for some, presumably mystical, reason the where key is set to true. Predictably, req.query.test returns the object now.
As with Go, exploitation will mostly depend on what the application expects these values to be, and how it behaves when they’re something else. Although since we have more control over how and what the query object will return there’s more avenues for successful exploitation.
Python Flask
Wrapping things up, let’s take a look at the Flask library for Python.
code used for this test
from flask import Flask, request
app = Flask(__name__)
@app.route('/', methods=['POST', 'GET'])
def test():
print("Value of request.args.get is: ")
print(request.args.get('test'))
print("Value of request.form.get is: ")
print(request.form.get('test'))
return "test"
if __name__ == "__main__":
app.run(debug=True)Similarly to Express, we have a request object with an args property with a convenient get method that returns the value for a specific queryparam. Sadly, this is where the similarities with Express end, as Flask will always return the first value for a given query param:
curl http://localhost:5000/?test=flask&test=carton

Like Go, POST data is handled using a different function request.form.get :

This function follows the same logic as request.args.get in regard to multiple parameters, only the first instance of a given parameter is selected while the rest are ignored.
Unfortunately, unlike with Express there are no tricks using bracket notation or any other special characters, so no cool avenues for exploitation there :c.
Afterword
Aaaand this concludes our brief investigation into HTTP Parameter Pollution, thank you for taking the time to read and I hope you learned something new.
I’ll probably make a part 2 looking into more languages, I’m specially interested in seeing how other JavaScript frameworks deal with this.
If you have any questions or if you’d like to explain to me WTF is going on with the javascript weirdness you can reach me on twitter @polarizeflow
cheers!
References
- https://www.imperva.com/learn/application-security/http-parameter-pollution/
- https://portswigger.net/web-security/api-testing/server-side-parameter-pollution
- https://medium.com/@0UN390/what-is-http-parameter-pollution-a50249869d97
- https://appcheck-ng.com/deep-dive-http-parameter-pollution/
- https://securityintelligence.com/posts/how-to-prevent-http-parameter-pollution/
- https://medium.com/@rramgattie/exploiting-parameter-pollution-in-golang-web-apps-daca72b28ce2