Some of the most serious findings in web and API tests are also the simplest. A user signs in, opens one of their own records, changes the ID in the request, and gets back a record that belongs to someone else. This is called an insecure direct object reference, or IDOR. It falls under Broken Access Control, which sits at the top of the OWASP Top 10, and Broken Object Level Authorization, first on the OWASP API Security Top 10.
What it looks like
Imagine an invoicing app with this endpoint:
GET /api/v2/invoices/48213 Authorization: Bearer <token for Company A>
The server checks that the token is valid, so the user is logged in. It then loads invoice 48213 and returns it. What it never checks is whether invoice 48213 belongs to Company A. Change the number to 48214 and the API returns Company B's invoice.
The bug is the missing line, not a broken one. Authentication answers “who are you?”. Authorisation answers “are you allowed to see this?”. IDOR happens when the second question is skipped.
Why scanners usually miss it
Automated scanners look for patterns: error messages, injected payloads reflected back, known vulnerable versions. An IDOR response looks completely normal. It returns 200 OK with valid JSON, just for the wrong customer. A tool can't tell that it's wrong without understanding who owns which data. A tester can, by signing in as two different users and swapping their identifiers.
Where to look
- Numeric or sequential IDs in paths and query strings:
/orders/1001,?user_id=57 - IDs in request bodies, especially on update and delete actions, which are often less carefully protected than reads
- File downloads and exports:
/reports/download?file=report_57.pdf - Mobile app APIs, which teams sometimes assume nobody will inspect
- GraphQL queries that accept an
idargument on many object types
Random IDs such as UUIDs make guessing harder, but they aren't a fix. IDs leak through URLs, logs, emails and other API responses.
How to fix it properly
- Check ownership on every request for an object. Load the record, compare its owner, tenant or organisation with the signed-in user, and return 404 if they don't match.
- Scope queries to the user from the start. Query
WHERE id = ? AND tenant_id = ?instead of fetching by ID alone, so another tenant's data never leaves the database. - Centralise the check. Put authorisation in middleware, a policy layer or a shared helper, so it can't be forgotten on the next new endpoint.
- Test with two accounts. Add automated tests that sign in as user A and try to read, update and delete user B's objects. Every one should fail.
Quick self-check: pick any endpoint that takes an ID. Can you point to the exact line of code that checks the current user is allowed to access that specific object? If not, it's worth testing.
How we test for it
In our web and API penetration tests, we map every endpoint that takes an identifier, then replay requests across users, roles and tenants. Each confirmed issue is reported with the exact request, the data exposed and the fix, and we retest after you've patched it.