Summary
By Sid — Cybersecurity Engineer & Pentester
Disclosure note: This was found during an authorized, client-commissioned penetration test. The target platform, endpoint domain, employee names, and all captured values below have been anonymized/fictionalized — none of the identifiers, credentials, or personal data shown are real. Reported to the client, remediation done/confirmed as of 08/24/26.
TL;DR
During a gray-box pentest of a corporate SaaS platform (“TargetApp”), an internal HR/personnel endpoint accepted a plain sequential integer — idPersonnal
— as the only identifier for which employee record to return, with no check that the authenticated caller had any relationship to that record. Walking the ID up or down returned full personnel records for other employees: name, department, supervisor, RFC-equivalent tax ID, phone, email, and role — with no rate limiting and no additional authorization step. This is API1:2023 – Broken Object Level Authorization (BOLA) from the OWASP API Security Top 10, and it turned a single authenticated low-privilege session into a full employee directory dump.
Get Sid’s stories in your inbox
Join Medium for free to get updates from this writer.
Severity: Critical (CVSS 3.1: 9.8
) Class: API1:2023 — Broken Object Level Authorization (OWASP API Security Top 10)
Background: what is BOLA?
BOLA is the API-specific cousin of the classic IDOR: an endpoint uses a client-supplied identifier to fetch an object, and the server trusts that identifier without verifying the caller actually owns or is entitled to that specific object. OWASP describes it plainly — object-level authorization is an access control mechanism that should be enforced every time an object is accessed via an identifier, and its absence is one of the most common and highest-impact flaws in API design, because unlike a UI-level access-control gap, an API endpoint often has no equivalent “you can’t click that button” visual cue hiding the problem.
Recon: where the endpoint came from
The account used for testing was a standard authenticated user with no administrative role. While mapping the platform’s API traffic in Burp Suite during normal navigation of the HR/personnel module, a request stood out:
POST /Personnal/SelectPersonnal HTTP/1.1
Host: api.targetapp.example.com
The request body carried an idPersonnal
field alongside a handful of filter options:
{“option”:6,“idPersonnal”:5741,“noEmployee”:"",“idLocation”:0,“idCenter”:0,“password”:""}
5741
— a short, sequential-looking integer — was the value the frontend had filled in automatically for the logged-in user’s own record. That’s the tell to look for in almost every BOLA case: an ID that looks small, incremental, and guessable, sitting in a field the client fully controls.
Exploitation
POST /Personnal/SelectPersonnal HTTP/1.1
Host: api.targetapp.example.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: application/json, text/plain, /
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs…
- Confidentiality: full enumeration of the organization’s personnel data — names, departments, supervisors, government tax IDs, phone numbers, and (due to the compounding issue above) plaintext passwords — for every employee in the system, not just the tester’s own account.
- Integrity / further compromise: the harvested plaintext credentials were independently confirmed to be valid, directly enabling account takeover for other employees, including accounts with elevated/administrative roles.
- Scale: no rate limiting was observed on the endpoint, meaning the entire directory was extractable via a simple automated loop — this is not a “one victim” bug, it’s a mass-enumeration bug. Root cause The endpoint’s handler used idPersonnal purely as a database lookup key, with the query logic effectively reading as “return the record matching this ID” rather than “return this record if the authenticated user is entitled to see it.” There was no server-side check binding the requested idPersonnal to the identity or role encoded in the session’s bearer token — object-level authorization was never enforced, only object-level retrieval. Remediation
- Enforce object-level authorization on every request that includes an identifier. Before returning any record, the server must verify that the authenticated user either owns that idPersonnal or holds an explicit, role-based grant to view records outside their own (e.g., HR/admin roles).
Pseudo-code
def select_personnal(request): requested_id = request.json.get(“idPersonnal”) caller = get_authenticated_user(request) if requested_id != caller.idPersonnal and not caller.has_role(“hr_admin”): raise ForbiddenError() record = db.get_personnal(requested_id) return serialize_for_role(record, caller.role) # never return password/email to non-privileged roles Conclusion BOLA earns its top spot on the OWASP API Top 10 for a simple reason: APIs hand attackers exactly the kind of interface — structured, predictable, identifier-driven — that makes this class of bug trivial to find and devastating to exploit once found. Here, a single unguarded integer parameter was the only thing standing between one authenticated session and an entire organization’s personnel directory, credentials included. Object-level authorization has to be checked on every object access, every time — never inferred from the fact that a request “looks” like it came from a legitimate, logged-in user.