HTTP Parameter Pollution in 2024 !
Hi after going through all the Black Hat and DEFCON web security researches in 2024 , I noticed that the easiest way to break web apps is by understanding how components handle your requests so in this write-up , I’m going to explain how languages and frameworks handle our parameters .
💬 JUST❗
I didn’t intend to see how languages and frameworks handle our parameters but since my setup requires a back-end I gave it some time .
In the beginning , I started to see which delimiter is being used to split our parameter .
⚠️ Ruby
It was known to me that Ruby uses the & and ; delimiters to split parameters .
1️⃣ PHP 8.3.11 AND Apache 2.4.62
<!DOCTYPE html>
<html>
<body>
<?php
echo $_GET['name'];
?>
</body>
</html>
<!DOCTYPE html>
<html>
<body>
<?php
echo $_POST['name'];
?>
</body>
</html>
1️⃣.1️⃣ Ignore anything after %00 in the parameter name .
1️⃣.2️⃣ Handle name[]=one as array .
1️⃣.3️⃣ handle name=one&name=two as name=two .
1️⃣.4️⃣ handle name=one&name[]=two as array .
1️⃣.5️⃣ handle name[]=one&name=two as name=two .
1️⃣.6️⃣ handle name[]=one&name[]=two as array .
1️⃣.7️⃣ handle GET Request name=one AND body name=two as name=one .
1️⃣.8️⃣ handle POST Request name=two AND body name=one using _GET as name=two .
1️⃣.9️⃣ handle POST Request name=two AND body name=one using _POST as name=one .
💬 Our PHP Parameter Pollution Summary in 2024❗
2️⃣ Ruby 3.3.5 AND WEBrick 1.8.2
require 'webrick'
def getPOC(query_param)
"<html><body>#{query_param}</body></html>"
end
server = WEBrick::HTTPServer.new(Port: 8000)
server.mount_proc '/' do |req, res|
param = req.query['name']
result = getPOC(param)
res['Content-Type'] = 'text/html'
res.body = result
end
trap 'INT' do server.shutdown end
server.start
2️⃣.1️⃣ Not Recognized name[]=one .
2️⃣.2️⃣ handle name=one&name=two as name=one .
1️⃣.3️⃣ handle name=one&name[]=two as name=one .
1️⃣.4️⃣ handle name[]=one&name=two as name=two .
1️⃣.5️⃣ Not Recognized name[]=one&name[]=two .
1️⃣.6️⃣ handle GET Request name=one AND body name=two as name=one .
1️⃣.7️⃣ handle POST Request name=two AND body name=one as name=one .
💬 Our Ruby Parameter Pollution Summary in 2024❗
3️⃣ Spring MVC 6.0.23 AND Apache Tomcat 10.1.30
@GetMapping("/getMapping")
@ResponseBody
String getMapping(@RequestParam(required = false, name = "name") String name) {
return "<!DOCTYPE html><html><body>" + name + "</body></html>";
}
@PostMapping("/postMapping")
@ResponseBody
String postMapping(@RequestParam(required = false, name = "name") String name) {
return "<!DOCTYPE html><html><body>" + name + "</body></html>";
}
@RequestMapping("/requestMapping")
@ResponseBody
String requestMapping(@RequestParam(required = false, name = "name") String name) {
return "<!DOCTYPE html><html><body>" + name + "</body></html>";
}
3️⃣.1️⃣ handle name[]=one :-
GetMapping as status=400
PostMapping as one
GET RequestMapping as status=400
POST RequestMapping as one
3️⃣.2️⃣ handle name=one&name=two :-
GetMapping as one,two
PostMapping as one,two
GET RequestMapping as one,two
POST RequestMapping one,two
3️⃣.3️⃣ handle name=one&name[]=two :-
GetMapping as status=400
PostMapping as one
GET RequestMapping as status=400
POST RequestMapping as one
3️⃣.4️⃣ handle name[]=one&name=two :-
GetMapping as status=400
PostMapping as two
GET RequestMapping as status=400
POST RequestMapping as two
3️⃣.5️⃣ handle name[]=one&name[]=two :-
GetMapping as status=400
PostMapping as one,two
GET RequestMapping as status=400
POST RequestMapping as one,two
3️⃣.6️⃣ handle query name=one AND body name=two without Content-Type :-
GetMapping as one
PostMapping as status=405
GET RequestMapping as one
POST RequestMapping as one
3️⃣.7️⃣ handle query name=two AND body name=one with Content-Type :-
GetMapping as status=405
PostMapping as two,one
GET RequestMapping as two
POST RequestMapping as two,one
💬 Our Spring MVC Parameter Pollution Summary in 2024❗
4️⃣ NodeJS 20.17.0 AND Express 4.21.0
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (request, response) => {
const nameInput = request.query.name;
var html = `
<!DOCTYPE html>
<html>
<body>${nameInput}</body>
</html>
`;
response.send(html);
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}/`);
});
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const port = 3000;
app.use(bodyParser.urlencoded({ extended: true }));
app.post('/', (request, response) => {
const nameInput = request.body.name;
var html = `
<!DOCTYPE html>
<html>
<body>${nameInput}</body>
</html>
`;
response.send(html);
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}/`);
});
4️⃣.1️⃣ handle name[]=one as one .
4️⃣.2️⃣ handle name=one&name=two as one,two .
4️⃣.3️⃣ handle name=one&name[]=two as one,two .
4️⃣.4️⃣ handle name[]=one&name=two as one,two .
4️⃣.5️⃣ handle name[]=one&name[]=two as one,two .
4️⃣.6️⃣ handle GET Request name=one AND body name=two as name=one .
4️⃣.7️⃣ handle POST Request name=two AND body name=one as name=one .
💬 Our NodeJS Parameter Pollution Summary in 2024❗
5️⃣ GO 1.22.7
package main
import (
"html/template"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
//r.ParseForm()
//nameValue := r.Form.Get("name")
nameValue := r.URL.Query().Get("name")
var tmpl = `<!DOCTYPE html><html><body>` + nameValue + `</body></html>`
t := template.New("main")
t, _ = t.Parse(tmpl)
t.Execute(w, "Hello")
}
func main() {
server := http.Server{
Addr: "0.0.0.0:8000",
}
http.HandleFunc("/", handler)
server.ListenAndServe()
}
5️⃣.1️⃣ Not Recognized name[]=one .
5️⃣.2️⃣ handle name=one&name=two as name=one .
5️⃣.3️⃣ handle name=one&name[]=two as name=one .
5️⃣.4️⃣ handle name[]=one&name=two as name=two .
5️⃣.5️⃣ Not Recognized name[]=one&name[]=two .
5️⃣.6️⃣ handle GET Request name=one AND body name=two as name=one .
5️⃣.7️⃣ handle POST Request name=two AND body name=one as name=one .
💬 Our GO Parameter Pollution Summary in 2024❗
6️⃣ Python 3.12.6 AND Werkzeug 3.0.4 AND Flask 3.0.3
from flask import request
from flask import Flask
app = Flask(__name__)
@app.route('/',methods=['GET'])
def index():
getName = "<!DOCTYPE html><html><body>" + request.args.get('name') + "</body></html>"
return getName
if __name__ == '__main__':
app.run(debug=True)
from flask import request
from flask import Flask
app = Flask(__name__)
@app.route('/',methods=['POST'])
def index():
getName = "<!DOCTYPE html><html><body>" + request.form.get('name') + "</body></html>"
return getName
if __name__ == '__main__':
app.run(debug=True)
6️⃣.1️⃣ Not Recognized name[]=one .
6️⃣.2️⃣ handle name=one&name=two as name=one .
6️⃣.3️⃣ handle name=one&name[]=two as name=one .
6️⃣.4️⃣ handle name[]=one&name=two as name=two .
6️⃣.5️⃣ Not Recognized name[]=one&name[]=two .
6️⃣.6️⃣ handle GET Request name=one AND body name=two as name=one .
6️⃣.7️⃣ handle POST Request name=two AND body name=one as name=one .
💬 Our Flask Parameter Pollution Summary in 2024❗
7️⃣ Python 3.12.6 AND Django 4.2.15
from django.http import HttpResponse
from django.shortcuts import render
def index(request):
getName = "<!DOCTYPE html><html><body>" + request.GET.get('name') + "</body></html>"
return HttpResponse(getName)
from django.http import HttpResponse
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def index(request):
getName = "<!DOCTYPE html><html><body>" + request.POST.get('name') + "</body></html>"
return HttpResponse(getName)
7️⃣.1️⃣ Not Recognized name[]=one .
7️⃣.2️⃣ handle name=one&name=two as name=two .
7️⃣.3️⃣ handle name=one&name[]=two as name=one .
7️⃣.4️⃣ handle name[]=one&name=two as name=two .
7️⃣.5️⃣ Not Recognized name[]=one&name[]=two .
7️⃣.6️⃣ handle GET Request name=one AND body name=two as name=one .
7️⃣.7️⃣ handle POST Request name=two AND body name=one as name=one .
💬 Our Django Parameter Pollution Summary in 2024❗
8️⃣ Python 3.12.6 AND Tornado 6.4.1
import asyncio
import tornado
class MainHandler(tornado.web.RequestHandler):
def get(self):
getName = "<!DOCTYPE html><html><body>" + self.get_query_argument('name', default=None) + "</body></html>"
self.write(getName)
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])
async def main():
app = make_app()
app.listen(8000)
await asyncio.Event().wait()
if __name__ == "__main__":
asyncio.run(main())
import asyncio
import tornado
class MainHandler(tornado.web.RequestHandler):
def post(self):
getName = "<!DOCTYPE html><html><body>" + self.get_body_argument('name', default=None) + "</body></html>"
self.write(getName)
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])
async def main():
app = make_app()
app.listen(8000)
await asyncio.Event().wait()
if __name__ == "__main__":
asyncio.run(main())
8️⃣.1️⃣ Not Recognized name[]=one .
8️⃣.2️⃣ handle name=one&name=two as name=two .
8️⃣.3️⃣ handle name=one&name[]=two as name=one .
8️⃣.4️⃣ handle name[]=one&name=two as name=two .
8️⃣.5️⃣ Not Recognized name[]=one&name[]=two .
8️⃣.6️⃣ handle GET Request name=one AND body name=two as name=one .
8️⃣.7️⃣ handle POST Request name=two AND body name=one as name=one .
💬 Our Tornado Parameter Pollution Summary in 2024❗
📝🧵 KEYPOINTS :-
— — — — — — — — — — — — — — — — — — — — — — — — — — —
1️⃣ PHP 8.3.11 AND Apache 2.4.62
1️⃣.1️⃣ Ignore anything after %00 in the parameter name .
1️⃣.2️⃣ Handle name[] as array .
1️⃣.3️⃣ _GET not meaning GET Method .
1️⃣.4️⃣ Prefer the last parameter .
2️⃣ Ruby 3.3.5 AND WEBrick 1.8.2
2️⃣.1️⃣ Uses the & and ; delimiters to split parameters .
2️⃣.2️⃣ Not Recognized name[] .
2️⃣.3️⃣ Prefer the first parameter .
3️⃣ Spring MVC 6.0.23 AND Apache Tomcat 10.1.30
3️⃣.1️⃣ POST RequestMapping == PostMapping & GET RequestMapping == GetMapping .
3️⃣.2️⃣ POST RequestMapping & PostMapping Recognized name[] .
3️⃣.3️⃣ Prefer name if name AND name[] existing .
3️⃣.4️⃣ Concatenate parameters e.g. first,last .
3️⃣.5️⃣ POST RequestMapping & PostMapping Recognized query parameter with Content-Type .
4️⃣ NodeJS 20.17.0 AND Express 4.21.0
4️⃣.1️⃣ Recognized name[] .
4️⃣.2️⃣ Concatenate parameters e.g. first,last .
5️⃣ GO 1.22.7
5️⃣.1️⃣ NOT Recognized name[] .
5️⃣.2️⃣ Prefer the first parameter .
6️⃣ Python 3.12.6 AND Werkzeug 3.0.4 AND Flask 3.0.3
6️⃣.1️⃣ NOT Recognized name[] .
6️⃣.2️⃣ Prefer the first parameter .
7️⃣ Python 3.12.6 AND Django 4.2.15
7️⃣.1️⃣ NOT Recognized name[] .
7️⃣.2️⃣ Prefer the last parameter .
8️⃣ Python 3.12.6 AND Tornado 6.4.1
8️⃣.1️⃣ NOT Recognized name[] .
8️⃣.2️⃣ Prefer the last parameter .
💬 Thanks
𝕏 0xAwali