IDOR: when changing a number in the URL reveals other users' data
A well-constructed REST API, a valid JWT token, and sequential IDs in the URLs. The rest is arithmetic. This case illustrates the most widespread confusion between authentication and authorisation: two distinct mechanisms that many applications only half-implement.
Context
A SaaS startup offers an appointment booking platform for consulting practices. Each practitioner manages their own clients through the interface. The data involved: names, emails, phone numbers, reasons for consultation, booked time slots.
The audit was requested by an investor before closing a funding round, a security report was part of the conditions. Two-day engagement, grey box: one standard practitioner account provided, no access to source code.
The discovery
Examining the application traffic after logging in, API requests follow a regular pattern: GET /api/appointments/4821. The identifier is numeric and visibly sequential.
I try accessing /api/appointments/4820, then /api/appointments/4819. Responses come back normally: client's full name, email, phone number, reason for consultation, the practitioner involved. Appointments that don't belong to the account I'm logged in with.
The server checked that the request contained a valid JWT token: that's authentication. It did not check that the requested appointment belonged to the authenticated practitioner: that's authorisation. Both are required. Only one was implemented.
The impact
Any authenticated user could access all appointments across the platform (from all practitioners), by iterating through IDs. No specialised tool required: a simple for loop is enough.
- Exposure of personal data for all clients across the platform
- Potentially sensitive consultation reasons (medical or legal context depending on the practice)
- GDPR breach with mandatory notification to the relevant supervisory authority
- Risk of a leak before the funding round, exactly the scenario the investor was trying to prevent
Authenticating proves who you are. Being authorised proves what you're allowed to access. These are two different things, and one does not replace the other.
The fix
The fix is architectural: every endpoint that returns a resource must verify that the authenticated user is the owner of that resource, or holds an explicit right to it. This is not a one-line fix added here and there; it's a systematic control.
The right approach: for every GET /api/appointments/{id} request, the backend retrieves the appointment, compares its owner_id field against the user ID in the JWT token, and returns 403 if they don't match. A central, unit-tested logic applied to every relevant endpoint.
Retesting after the fix confirmed that cross-account access was blocked. The investor received the report with the vulnerability documented, the fix verified, and the status marked "resolved". The funding round was able to close.
The takeaway
IDOR (Insecure Direct Object Reference) has featured regularly in the OWASP Top 10 for years. It persists because modern frameworks make authentication straightforward but don't enforce object-level authorisation: that's left to the developer to implement. Without an external review, it often goes unnoticed in functional tests, which verify what the application is supposed to do, not what it should prevent.
Random IDs (UUID v4) would make enumeration harder, but they don't replace a real authorisation check. Obscurity is not security: if an attacker obtains an ID through another channel, the problem is unchanged.
Further reading
A question after reading?
Questions about how access control is handled in your API? Send a message. No pitch, no automatic quote.
Or book a 30-minute scoping call directly
Book a call →