IDOR Testing Checklist for QA Engineers
An insecure direct object reference (IDOR) happens when an application uses an ID from the request to fetch an object and never checks that the caller is allowed to have it. Change /api/invoices/1001 to /api/invoices/1002 and you're reading someone else's invoice. It's one of the most common serious bugs in web and mobile APIs, and QA engineers are well placed to find it.
In OWASP terms, IDOR is part of A01:2021 Broken Access Control, which is first on the OWASP Top 10. In the OWASP API Security Top 10 it's called API1:2023 Broken Object Level Authorization (BOLA).
Only test applications you're authorized to test: your own product, a staging environment you have permission for, or a bug bounty programme whose scope includes the target.
1. Set up two accounts (and roles)
- Create User A and User B at the same role level, in different organizations or tenants if the product has them.
- If the product has roles, also create a low-privilege user (viewer) and a high-privilege user (admin).
- Give each account its own data: orders, files, messages, profile and payment methods.
2. Map every object reference
Use the application as User A with Burp Suite proxying the traffic. Then look through the HTTP history for anything that identifies an object:
- Path parameters:
/users/57/settings - Query parameters:
?accountId=57,?file=report-57.pdf - JSON bodies:
{"orderId": 1042}, including nested and array fields - Headers and cookies that carry a user or tenant ID
- GraphQL arguments:
order(id: "1042") - Export, download and print endpoints, which often get forgotten
3. Swap the IDs
Send each request to Repeater. Keep User A's session, replace the ID with one of User B's, and compare the response.
- Read: can A see B's object?
- Write: can A update B's object with
PUTorPATCH? Then log in as B and check whether it actually changed. - Delete: can A delete B's object?
- Create on behalf of: can A create an object with
"ownerId"set to B? - Other methods: if
GETis protected, tryPOST,PUTandDELETEon the same path. - Other versions: if
/api/v2/is protected, try/api/v1/.
A 200 response isn't proof on its own, so check the body. And a 403 on the read doesn't prove the write is safe.
4. Don't be fooled by "unguessable" IDs
UUIDs make IDs hard to guess, but they don't fix access control. Real applications leak IDs all the time: in other API responses, shared links, email notifications, exports, logs and URLs. If the server doesn't check ownership, any leaked UUID is enough to get in.
5. Scale it up with Autorize
The Autorize extension for Burp Suite replays every request you make with a second user's session (and with no session at all). It flags the ones that still succeed. Browse the whole application as the high-privilege user and let Autorize test it with the low-privilege session. It's the fastest way to cover a large app, but confirm every flagged request by hand before you report it.
6. Report it so it gets fixed
- Title: what one user can do to another, e.g. "Any user can read other customers' invoices via /api/invoices/{id}".
- Steps: the two accounts, the exact request and the changed ID.
- Impact: what data or actions are exposed, and how many users are affected.
- Fix: check object ownership on the server for every request, ideally in one shared authorization layer and not separately in each endpoint.
7. Turn every finding into a regression test
This is where QA adds most value. Once a developer fixes the bug, add an automated test so it can't come back:
test("user A cannot read user B's invoice", async ({ request }) => {
const res = await request.get(`/api/invoices/${fixtures.userB.invoiceId}`, {
headers: { Authorization: `Bearer ${fixtures.userA.token}` },
});
expect([403, 404]).toContain(res.status());
});
Run these in CI with the rest of your suite. I describe the full setup in QA automation and security testing in one CI pipeline.
The checklist
- Two users per role, each with their own data.
- Every object reference mapped: path, query, body, headers, GraphQL and exports.
- Read, write, delete and create-as-another-user tested for each one.
- Other HTTP methods and older API versions tried.
- UUID-based endpoints tested with IDs leaked from elsewhere.
- The whole app replayed with Autorize as a low-privilege and an unauthenticated user.
- Every confirmed finding reported with impact and a clear fix.
- Every fix covered by an automated regression test.