August 27, 2026
PortSwigger Lab Walkthrough: User ID Controlled by Request Parameter, with Unpredictable User IDs
Introduction

By Vivek Yadav
4 min read
Introduction
In my last write-up, I covered a straightforward horizontal privilege escalation lab where user accounts were identified by plain usernames in a URL parameter — trivially guessable, trivially exploitable. PortSwigger's Web Security Academy follows that lab up with a more realistic variant: "User ID Controlled by Request Parameter, with Unpredictable User IDs."
This version introduces a common (and flawed) developer assumption: "If the ID is hard to guess, it's secure enough." Spoiler — it isn't. This lab is a perfect demonstration of why security through obscurity fails the moment an identifier leaks somewhere else in the application.
Lab Objective
Per PortSwigger's description, this lab has a horizontal privilege escalation vulnerability on the user account page — but this time, the app identifies users with GUIDs (globally unique identifiers) instead of usernames.
The goal: find the GUID belonging to carlos, use it to access his account, and submit his API key as the solution.
We're given the same starting credentials as before:
Username: wiener
Password: peterUsername: wiener
Password: peterWhy GUIDs Alone Aren't a Fix
Before diving into the walkthrough, it's worth pausing on why this lab exists. A lot of developers, upon learning about IDOR (Insecure Direct Object Reference) vulnerabilities, reach for a quick fix: swap sequential or predictable IDs (like usernames, or id=1, id=2) for long, random GUIDs like a2bf8243-d5a5-49c0-8799-6287a17a7ede.
The logic seems sound at first — you can't brute-force or guess a 128-bit random value. But this only holds up if the GUID never appears anywhere else that an attacker can reach. As we're about to see, that assumption is fragile.
Setting Up
As usual, I had Burp Suite running to inspect traffic, though this lab ended up being solvable almost entirely through the browser UI, with Burp mainly useful for double-checking requests. Sometimes the simplest path to exploitation isn't a proxy tool — it's just clicking around the app like a curious user would.
Step-by-Step Walkthrough
1. Logging in
I logged in using the provided credentials, wiener:peter, and landed on the account dashboard.
2. Exploring the navigation
At the top right of the page, there were three options: Home, My Account, and Log Out. My own account page showed a URL parameter identifying me — something like:
/my-account?id=[wiener's-guid]/my-account?id=[wiener's-guid]Confirming that the app does indeed use GUIDs to identify accounts, not usernames. Directly guessing or brute-forcing carlos's GUID here was off the table — that's precisely the point of this lab.
3. Finding a leak point
Since I couldn't guess carlos's ID, I needed to find somewhere else in the app that might expose it. I navigated to the Home page, which listed blog posts from various users — and one of them was written by carlos.
4. Reading the blog post
I clicked into carlos's blog post. Nothing unusual in the content itself, but I noticed the author's name, "carlos," displayed at the top of the post — and it was styled as a clickable link, not plain text.
5. Discovering the GUID in the URL
Clicking carlos's name took me to a URL like this:
/blogs?userId=a2bf8243-d5a5-49c0-8799-6287a17a7ede/blogs?userId=a2bf8243-d5a5-49c0-8799-6287a17a7edeAnd there it was — carlos's GUID, exposed in plain sight through a completely unrelated feature (the blog author link), with zero authentication or access control around it.
This is the crux of the vulnerability: the GUID may be unguessable, but it isn't unreachable. The application itself leaked it through a different, seemingly unrelated code path.
6. Copying the GUID
I copied the full GUID: a2bf8243-d5a5-49c0-8799-6287a17a7ede.
7. Swapping it into the account page URL
I navigated back to My Account, where the URL contained my own GUID in the id parameter. I replaced it with carlos's GUID:
/my-account?id=a2bf8243-d5a5-49c0-8799-6287a17a7ede/my-account?id=a2bf8243-d5a5-49c0-8799-6287a17a7ede8. Loading carlos's account
Hitting enter loaded the page — but instead of showing my account, it displayed carlos's account details, including his API key, rendered directly on the page.
Just like in the previous lab, the server was authenticating me as wiener via my session cookie, but was still trusting the id parameter from the URL to decide whose data to display. My session was valid, but it was never actually checked against the account being requested.
9. Extracting the API key
I copied carlos's API key from the page.
10. Submitting the solution
Pasted the key into the lab's solution field and submitted. The lab flagged as solved.
Root Cause Analysis
This is the same fundamental flaw as the earlier lab — a missing server-side authorization check on the account endpoint. The only difference is the identifier scheme.
What makes this variant more interesting is the false sense of security that GUIDs provide. The developers likely believed that switching from predictable usernames to random GUIDs would close the vulnerability. But this treats unpredictability as a substitute for authorization — and those are not the same thing.
The GUID being hard to guess only matters if it's also kept confidential. In this case, it wasn't. It was exposed through:
- A public-facing blog post
- An author link that anyone (even unauthenticated users, potentially) could click
This is a great real-world parallel to bugs found in production applications, where GUIDs, tokens, or internal IDs leak through:
- URL parameters in shared links
- API responses that include more fields than the frontend actually uses
- Referrer headers
- Client-side JavaScript source
- Error messages or debug logs
How This Should Be Fixed
The fix here is the same as before, with one added lesson:
- Authorization must never rely on the secrecy of an identifier. Whether it's
id=1orid=a2bf8243-d5a5-49c0-8799-6287a17a7ede, the server must verify that the authenticated session actually owns the resource being requested — every single time, regardless of how "random" the ID looks. - Audit where identifiers get exposed. Even values intended to be non-guessable can leak through unrelated features. Any place a user ID, GUID, or object reference appears — blog posts, comments, profile links, API responses — should be treated as a potential exposure point.
- Don't conflate obscurity with access control. Random doesn't mean private. If an ID needs to stay confidential, it needs to be treated as a secret, not just "hard to guess."
Key Takeaways
- Switching from predictable IDs to random GUIDs does not fix a broken access control vulnerability — it just raises the bar for guessing, not for authorization.
- Real-world exploitation often isn't about brute force — it's about finding where an application accidentally leaks information through an unrelated feature.
- Manual exploration of an app (clicking around like a normal, curious user) is sometimes just as powerful as automated tooling for uncovering these paths.
- Any identifier used for authorization should be validated server-side against the authenticated user — full stop, regardless of how it's generated.
This lab is a nice reminder that security isn't just about picking "stronger" values — it's about making sure the logic around those values actually enforces the boundaries you think it does..
GoodByeee….