Insecure Direct Object References
Introduction¶
This case study reproduces an insecure direct object reference (IDOR) against the WebGoat v2025.3 profile endpoint and traces the vulnerability to the specific authorization omission in the source. The objectives are threefold: to demonstrate empirical reproducibility of the exploit against a pinned target, to locate the absent control at the source level (file, class, method), and to map the findings to OWASP and CWE taxonomy.
Insecure direct object reference (IDOR) is a class of broken access control vulnerability, listed as API1:2023 Broken Object Level Authorization (BOLA) in the OWASP API Security Top 10 1. It occurs when a web application exposes an object identifier (in a URL path, query parameter, or response body) and trusts the client-supplied identifier without verifying that the authenticated user is allowed to act on that specific object. The consequence is unauthorized read or write of resources that belong to other users.
The vulnerability’s taxonomy, prevalence, and high-level mitigation principles are well established in OWASP guidance and reference documentation 2. This case study does not revisit those framings. Its contribution is the source-level evidence (file, class, method, and the specific missing check) that explains why the WebGoat implementation permits the exploit, alongside the reproducible empirical demonstration against the pinned target.
IDOR presents in three forms:
URL tampering. The most common form. The attacker swaps the identifier in a URL or path parameter to reach a resource owned by someone else.
Body or document manipulation. The identifier lives in the request body (JSON field, form input, hidden field). Tampering with it has the same effect as URL tampering.
File access. The identifier maps to a file path or storage key. Predictable or directly-referenced paths let the attacker read files outside their own scope.
Two controls mitigate IDOR:
Authorize every object access on the server 3. For every request that touches an object, verify the authenticated user is allowed to perform the requested action on that specific object. This is the only control that closes IDOR; everything else is defense in depth.
Use opaque, unguessable identifiers. UUIDs or random tokens raise the cost of enumeration but do not replace authorization checks. An attacker who learns a single valid identifier still gets through if no authorization runs.
Materials¶
WebGoat v2025.3
docker run --rm -p 8080:8080 -p 9090:9090 webgoat/webgoat:v2025.3mitmproxy v12.2.0
mitmproxy --mode reverse:http://localhost:8080 --listen-port 8888Methods¶
This case study reproduces the IDOR vulnerability in the WebGoat profile endpoint at the pinned tag v2025.3. The investigation proceeds in three phases: authenticate against the lesson with the documented test credentials, intercept the /IDOR/profile request and inspect the response for fields the UI does not surface, then substitute the recovered identifier in subsequent requests to read and modify other accounts’ profiles. The read and write steps rely on standard HTTP method semantics 4, with GET retrieving the profile representation and PUT replacing it. Requests are observed through mitmproxy in reverse-proxy mode (see Materials). The success criterion is the unauthorized read of another account’s profile and an unauthorized write that escalates that account’s role.
Results¶
The exploit hinges on two observations: the profile response body carries an identifier the UI hides, and the parameterized endpoint returns any profile that identifier names. The full attack flow is summarized in Figure 1.
Figure 1:Attack flow against the IDOR profile endpoint, from authentication through unauthorized read and privilege escalation.
Authentication requires the credentials tom and cat for the username and password respectively. Submitting them through the login form establishes the session.

Figure 2:Submitting tom / cat against the WebGoat IDOR lesson login form.
After authentication, the application’s responses diverge from what the UI presents. The operative question is what the server sends back versus what the UI renders.
Clicking the View Profile button reveals the user’s profile. The UI surfaces three pieces of information: the user’s name, color, and size.

Figure 3:The View Profile panel surfaces only name, color, and size.
Inspecting the source code of the page reveals the endpoint that retrieves the profile: GET /WebGoat/IDOR/profile.

Figure 4:Page source reveals the endpoint GET /WebGoat/IDOR/profile behind the View Profile button.
Intercepting the endpoint and examining the response shows that the application is returning additional information that is not visible in the UI, including the user’s role and userId.

Figure 5:The mitmproxy capture of the GET /WebGoat/IDOR/profile request.

Figure 7:The lesson answer form accepts the userId recovered from the intercepted response.
With the hidden userId exposed in the response body, it may be possible to access other users’ profile information through path manipulation. The application is not enforcing server-side access control on the profile endpoint, nor does it use an opaque identifier that would resist guessing.
Adding tom’s own userId to the path reveals the profile information of their account.

Figure 8:Appending tom’s own userId to the path returns the same profile, confirming the endpoint accepts a path-based identifier.
Confirming that tom’s own userId resolves through the path means the same mechanism can be turned against other accounts. Intercepting the View Profile request and substituting a different account’s userId causes the server to return that account’s profile.

Figure 9:The View Profile button that triggers the parameterized profile request.

Figure 10:The intercepted View Profile request, paused for path modification before forwarding.
Incrementing or decrementing the userId reveals the profile information of the next or previous user in the database. Repeating this process enumerates additional profiles.
The next user’s userId is not known a priori; incrementing from tom’s known value converges on /WebGoat/IDOR/profile/2342388, which returns the profile of Buffalo Bill.

Figure 11:Substituting 2342388 in the path returns Buffalo Bill’s profile.
Finally, the database state can be modified by changing the request method and supplying different values for known fields.

Figure 12:The lesson prompts for modification of another account’s profile.
WebGoat models privilege as an inverse number: a lower role value means higher privilege. Escalating Buffalo Bill therefore requires lowering their role from 4 to 1; setting color to red in the same request serves as a side-effect that confirms the write succeeded. Sending a PUT request to the /WebGoat/IDOR/profile/2342388 endpoint with the following payload performs the escalation:
{
"role": 1,
"color": "red",
"size": "large",
"name": "Buffalo Bill",
"userId": 2342388
}The server is expecting a json object with the following fields: role, color, size, name, and userId. Therefore, all fields must be passed in the payload. Additionally, the Content-Type request header must be set to application/json.

Figure 13:The original GET request captured before method rewriting.

Figure 14:The same request rewritten as a PUT with a JSON body and Content-Type: application/json.
The response from the server shows that the request was successful and the role of Buffalo Bill was updated to 1 and the color was changed to red.

Figure 15:The server response confirms the write: Buffalo Bill’s role is now 1 and color is red.

Figure 16:The WebGoat lesson UI marks the profile-modification step complete.
Discussion¶
The two vulnerable handlers live in single-class controllers under org/owasp/webgoat/lessons/idor/ at the v2025.3 tag 5. IDORViewOtherProfile.completed maps GET /IDOR/profile/{userId} (Program 1), and IDOREditOtherProfile.completed maps PUT /IDOR/profile/{userId} (Program 2). Both accept the path variable directly and pass it to new UserProfile(userId) to perform the lookup and (for the PUT handler) the field-level mutation, without comparing the path-supplied userId to the session-bound principal stored in LessonSession under the key idor-authenticated-user-id. The source comments name the omission explicitly: the view handler concedes that “secure code would ensure there was a horizontal access control check prior to dishing up the requested profile”, and the edit handler is introduced with “accepting the user submitted ID and assuming it will be the same as the logged in userId and not checking for proper authorization”.
@GetMapping(path = "/IDOR/profile/{userId}", produces = {"application/json"})
@ResponseBody
public AttackResult completed(@PathVariable("userId") String userId) {
// ...
UserProfile requestedProfile = new UserProfile(userId);
// secure code would ensure there was a horizontal access control check
// prior to dishing up the requested profile
// ...
}Program 1:Vulnerable GET /IDOR/profile/{userId} handler from IDORViewOtherProfile.
@PutMapping(path = "/IDOR/profile/{userId}", consumes = "application/json")
@ResponseBody
public AttackResult completed(
@PathVariable("userId") String userId,
@RequestBody UserProfile userSubmittedProfile) {
String authUserId = (String) userSessionData.getValue("idor-authenticated-user-id");
// this is where it starts ... accepting the user submitted ID and assuming
// it will be the same as the logged in userId and not checking for proper
// authorization
UserProfile currentUserProfile = new UserProfile(userId);
// ...
currentUserProfile.setColor(userSubmittedProfile.getColor());
currentUserProfile.setRole(userSubmittedProfile.getRole());
}Program 2:Vulnerable PUT /IDOR/profile/{userId} handler from IDOREditOtherProfile.
That the omission is structural rather than a framework limitation is evident from the sibling handler IDORViewOwnProfile.invoke (Program 3), which maps GET /IDOR/profile with no path variable and uses the session-bound identity as the lookup parameter. The session-based identity is already present in the request context; the parameterized handlers simply ignore it in favor of the client-supplied path key. This is the defining shape of CWE-639 (Authorization Bypass Through User-Controlled Key) 6: the request carries the key, the server trusts the key, and no binding between the key and the authenticated principal is enforced before the privileged operation runs.
@GetMapping(path = {"/IDOR/own", "/IDOR/profile"}, produces = {"application/json"})
@ResponseBody
public Map<String, Object> invoke() {
String authUserId = (String) userSessionData.getValue("idor-authenticated-user-id");
UserProfile userProfile = new UserProfile(authUserId);
// ...
}Program 3:Safe sibling handler IDORViewOwnProfile.invoke, deriving identity from LessonSession.
The counterfactual is a single guard at the top of each parameterized handler (Program 4), before the UserProfile is constructed from the path-supplied userId. For an endpoint scoped to per-user profile data, ownership equality is the correct rule, and the guard applies symmetrically to both the read and the write path. Cases with a legitimate cross-user mode (administrators editing other accounts) would extend the guard with a role check rather than weaken it. The fix is independent of the rest of the lesson logic and changes nothing about the request shape that the client sees.
String authUserId = (String) userSessionData.getValue("idor-authenticated-user-id");
if (!userId.equals(authUserId)) {
throw new AccessDeniedException("not authorized for this profile");
}Program 4:Minimal ownership guard closing both the read enumeration and the privilege-escalation write.
The taxonomy mapping follows directly. The weakness class is CWE-639, the user-controlled-key specialization of the more general improper-authorization class CWE-285 7; CWE-639 is the better fit because the defining feature of the empirical finding is that a client-supplied key is trusted without authorization, not that authorization is misconfigured in some broader sense. The API-surface category is OWASP API1:2023 Broken Object Level Authorization, where the missing control is per-object rather than per-function. The umbrella entry is OWASP A01:2025 Broken Access Control 8.
Conclusion¶
The demonstration produced both unauthorized reads and an unauthorized write against the WebGoat profile endpoint because neither control named in the introduction was present. The endpoint accepted a path-supplied userId without verifying that it belonged to the authenticated session, making every other user’s profile reachable; sequential numeric identifiers reduced enumeration to a tractable counting exercise, converging on Buffalo Bill at userId 2342388. Pivoting from GET to PUT against the same path lowered that account’s role from 4 to 1, confirming the absent ownership check applied to writes as well as reads.
Of the two mitigations named at the outset, server-side per-object authorization is the load-bearing control: an explicit ownership assertion before the lookup and before the update would have closed both the read enumeration and the privilege-escalation write. Opaque identifiers function as defense in depth rather than a substitute for authorization. They would have raised the cost of enumeration, but on their own would still have admitted any client that learned a single valid identifier.
- OWASP Foundation. (2023). API1:2023 Broken Object Level Authorization. OWASP API Security Top 10:2023. https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/
- Mozilla Developer Network. (n.d.). Insecure Direct Object References. MDN Web Docs. Retrieved May 24, 2026, from https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/IDOR
- OWASP Foundation. (n.d.). Authorization Cheat Sheet. OWASP Cheat Sheet Series. Retrieved May 24, 2026, from https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html
- Fielding, R., Nottingham, M., & Reschke, J. (2022). HTTP Semantics (Techreport RFC 9110). Internet Engineering Task Force. https://www.rfc-editor.org/rfc/rfc9110.html
- WebGoat Project Contributors. (2025). WebGoat: A deliberately insecure web application. GitHub repository, tag v2025.3. https://github.com/WebGoat/WebGoat/tree/v2025.3
- MITRE Corporation. (n.d.). CWE-639: Authorization Bypass Through User-Controlled Key. Common Weakness Enumeration. Retrieved May 24, 2026, from https://cwe.mitre.org/data/definitions/639.html
- MITRE Corporation. (n.d.). CWE-285: Improper Authorization. Common Weakness Enumeration. Retrieved May 24, 2026, from https://cwe.mitre.org/data/definitions/285.html
- OWASP Foundation. (2025). A01:2025 Broken Access Control. OWASP Top 10:2025.
