Insecure Direct Object Reference (IDOR)
Also known as: IDOR, BOLA, Broken Object Level Authorization, Broken access control
A broken access control flaw in which an endpoint returns or changes a record chosen by an identifier in the request without checking that the signed-in caller is allowed to touch that specific record.
How it works
Insecure direct object reference is a broken access control flaw. The application exposes an identifier for an object, such as an order number in a URL or a field in a request body, and the server uses that identifier to fetch or change the record. If the server never confirms that the caller is permitted to act on that particular object, any signed-in user can ask for someone else's record by supplying a different identifier.
The flaw is easy to miss because the application appears to work. The caller is authenticated, the route exists, and the record is returned. What is absent is the object-level decision: does this record belong to this caller, or does the caller hold a role that allows it? Sequential or guessable ids make the problem worse, but unguessable ids do not fix it, because an identifier that leaks through a log, an email or a shared link is just as usable.
The same gap appears on every verb. A read endpoint exposes data, while an update or delete endpoint lets a caller change or destroy records they do not own. Function-level gaps, such as a regular user reaching an administrative route, belong to the same family and are fixed by the same principle.
The defence is to make authorization a server-side decision on every request. Load the object, compare its owner or tenant with the authenticated caller, and refuse by default when the check does not pass. Scoping queries to the caller, using indirect references that map to the caller's own objects, and centralising the check so no handler can forget it all reduce the chance of a gap.
Walk through it
- 1Review the handler
- 2Confirm the finding
- 3Name the missing control
- Write the authorized handler
- Verify and monitor
A pre-release review flags the order detail endpoint. It returns an order by the id in the path. Read the handler as a reviewer and note what it checks before it returns the record.
1// GET /api/orders/:id2router.get("/api/orders/:id", requireSession, async (req, res) => {3 const order = await db.orders.findById(req.params.id);4 if (!order) return res.status(404).end();5 res.json(order);6});Spot it
- One account requesting many different object ids in a short window, especially sequential ones.
- A user id in the session that does not match the owner of the object returned with a 200 response.
- A near-absence of 403 or 404 responses on object endpoints, which suggests that checks are not being made.
- Requests for objects in another tenant or region than the caller's.
- Bulk read or enumeration patterns against /api/<resource>/{id} from a single session or client address.
Application access log
ts=2026-10-11T10:02:11Z user=u_204 method=GET path=/api/orders/10440 owner=u_204 status=200
ts=2026-10-11T10:02:12Z user=u_204 method=GET path=/api/orders/10441 owner=u_311 status=200
ts=2026-10-11T10:02:12Z user=u_204 method=GET path=/api/orders/10442 owner=u_377 status=200
ts=2026-10-11T10:02:13Z user=u_204 method=GET path=/api/orders/10443 owner=u_402 status=200API gateway
ts=2026-10-11T10:02:13Z client=203.0.113.50 session=s_9f2 distinct_object_ids_60s=48 denied=0splSplunk: one user reading objects owned by others
index=app sourcetype=access path="/api/orders/*" status=200
| where user!=owner
| stats dc(path) as objects count by user
| where objects > 5Expect legitimate support and admin roles; exclude them by role before alerting.
sqlSQL: sessions touching many distinct objects in a minute
SELECT user_id, COUNT(DISTINCT object_id) AS objects
FROM api_access_log
WHERE ts > NOW(), INTERVAL '1 minute' AND route = '/api/orders/:id'
GROUP BY user_id
HAVING COUNT(DISTINCT object_id) > 20;Stop it
Check object-level authorization on every request
After loading an object, verify on the server that the authenticated caller owns it or holds a role that permits the action. Do this for every verb, read and write, and never rely on the client to send only its own ids.
Deny by default and scope queries to the caller
Refuse access unless a rule explicitly grants it. Where possible, build the query from the caller's identity, so a record that belongs to someone else is simply never found.
Centralise the check and reduce what is exposed
Put the decision in shared middleware or a policy layer so no new handler can omit it. Use indirect references that map to the caller's own objects, and treat unguessable ids as a bonus, not a control.
Scope the lookup to the caller and deny by default
Vulnerable
router.get("/api/orders/:id", requireSession, async (req, res) => {
const order = await db.orders.findById(req.params.id);
res.json(order);
});Hardened
router.get("/api/orders/:id", requireSession, async (req, res) => {
const order = await db.orders.findOne({
id: req.params.id,
userId: req.session.user.id, // object-level check
});
if (!order) return res.status(404).end(); // deny by default
res.json(order);
});Returning 404 for objects the caller does not own avoids confirming that the id exists. Use your framework's policy layer where one exists.
- Enforce authorization on the server for every object endpoint and every HTTP verb.
- Deny by default and grant access through explicit ownership, tenant or role rules.
- Filter queries by the authenticated identity rather than trusting an id from the client.
- Centralise checks in middleware or a policy engine and require tests for each new route.
- Log authorization failures and alert on a single caller touching many distinct object ids.
- Add automated tests that call each endpoint as a second user and expect a refusal.
If it already happened
Deploy the ownership check or disable the affected endpoint, and rate-limit object lookups per session while the fix is tested.
Search access logs for sessions that read or changed objects they do not own, and list the affected records and users.
Notify affected users as your obligations require, reset anything that was altered, and confirm the fix with a second-account test on every verb.
Move the check into shared middleware, add a review checklist item for every new object route, and keep the log detection above.
Check yourself
1. What is the root cause of an insecure direct object reference?
2. Which fix addresses the flaw most directly?
3. Which log pattern most strongly suggests this flaw is being abused?