Skip to article content

Broken Access Control

A volume of empirical case studies on OWASP A01:2025 vulnerabilities in WebGoat

Back to Article
Insecure Direct Object References
Download Article

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:

Two controls mitigate IDOR:

Materials

WebGoat v2025.3

docker run --rm -p 8080:8080 -p 9090:9090 webgoat/webgoat:v2025.3

mitmproxy v12.2.0

mitmproxy --mode reverse:http://localhost:8080 --listen-port 8888

Methods

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.

WebGoat IDOR lesson login form with credentials tom and cat entered

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.

Profile panel rendered in the WebGoat UI showing only the name, color, and size fields

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.

Browser developer tools showing the page source with the GET /WebGoat/IDOR/profile endpoint highlighted

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.

mitmproxy capture of the GET /WebGoat/IDOR/profile request

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

Profile response body in mitmproxy showing extra role and userId fields beyond what the UI rendered

Figure 6:The response body carries role and userId beyond what the UI surfaces.

WebGoat lesson answer form filled with the userId recovered from the intercepted response

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.

Profile response after appending tom's own userId to the request path, confirming the endpoint accepts a path-based identifier

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.

View Profile button in the WebGoat IDOR lesson UI

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

mitmproxy intercept of the View Profile request, paused so the path can be edited before forwarding

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.

Profile response for /WebGoat/IDOR/profile/2342388 showing Buffalo Bill's account data after enumerating userId values

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.

WebGoat lesson prompt asking the user to modify values in another account's profile

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.

mitmproxy capture of the original GET request prior to method and body modification

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

Same request rewritten in mitmproxy as a PUT with a JSON body and Content-Type application/json

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.

Server response confirming the PUT succeeded, returning Buffalo Bill's updated profile with role 1 and color red

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

WebGoat lesson UI marking the profile-modification step complete

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.

References
  1. 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/
  2. 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
  3. 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
  4. Fielding, R., Nottingham, M., & Reschke, J. (2022). HTTP Semantics (Techreport RFC 9110). Internet Engineering Task Force. https://www.rfc-editor.org/rfc/rfc9110.html
  5. WebGoat Project Contributors. (2025). WebGoat: A deliberately insecure web application. GitHub repository, tag v2025.3. https://github.com/WebGoat/WebGoat/tree/v2025.3
  6. 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
  7. MITRE Corporation. (n.d.). CWE-285: Improper Authorization. Common Weakness Enumeration. Retrieved May 24, 2026, from https://cwe.mitre.org/data/definitions/285.html
  8. OWASP Foundation. (2025). A01:2025 Broken Access Control. OWASP Top 10:2025.