Insecure Direct Object Reference (IDOR)
debt(d9/e3/b5/t7)
Closest to 'silent in production until users hit it' (d9). The detection_hints explicitly state automated: no, and the why_it_matters field confirms it is 'invisible to automated scanners.' Semgrep is listed as a tool but the code_pattern requires detecting the absence of a subsequent ownership check — a logic-level property that static analysis cannot reliably verify. IDOR produces no error, no log anomaly, and no visible failure; it only manifests when an attacker actually accesses another user's data, typically discovered via manual penetration testing or user complaint.
Closest to 'simple parameterised fix' (e3). The quick_fix is a single ownership assertion ($resource->user_id === auth()->id()) added after every resource load. While one endpoint is a one-liner, the common_mistakes note that update and delete routes are commonly missed, meaning the fix must be applied as a pattern across all resource-loading endpoints — a small but consistent refactor within one component or controller layer, not a cross-cutting architectural change.
Closest to 'persistent productivity tax' (b5). IDOR applies broadly to all web and API contexts (applies_to: web, api) and the required ownership-check pattern must be enforced on every resource-fetching route indefinitely. Every new endpoint a developer writes carries the same risk and requires the same discipline, imposing an ongoing productivity tax across many work streams, though it does not reshape the system's architecture.
Closest to 'serious trap — contradicts how a similar concept works elsewhere' (t7). The misconception field directly captures the canonical trap: developers substitute UUID randomness for authorisation, believing obscurity equals security. Additionally, common_mistakes show that developers routinely conflate authentication (is the user logged in?) with authorisation (does this user own this resource?), a distinction that contradicts the mental model formed from simpler authentication-only systems. Both traps are well-documented yet consistently repeated by competent developers.
Also Known As
TL;DR
Explanation
IDOR occurs when an application exposes internal object identifiers (database IDs, filenames) directly in requests and does not verify that the requesting user is authorised to access that object. Example: /invoice?id=1234 — incrementing the ID to 1235 returns another user's invoice. Prevention: always verify that the authenticated user owns or has permission to access the requested resource, server-side, on every request.
How It's Exploited
# If order 1337 belongs to another user and there's no ownership check,
# attacker reads their private data by incrementing the ID
Diagram
flowchart TD
USER2[Logged in as User 42] --> REQ3[GET /api/invoices/100]
REQ3 --> CHECK2{Check: does user 42<br/>own invoice 100?}
CHECK2 -->|no check!| RETURN2[Returns invoice 100<br/>belongs to user 99!]
subgraph Fix3
AUTH3[Authorisation check<br/>SELECT WHERE id=100 AND user_id=42]
ZERO[0 rows = 404 Not Found<br/>never reveal resource exists]
end
style CHECK2 fill:#f85149,color:#fff
style RETURN2 fill:#f85149,color:#fff
style AUTH3 fill:#238636,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- Assuming sequential IDs are "hard to guess" — automated tools enumerate thousands of IDs per second.
- Checking authentication but not ownership — verifying a user is logged in does not verify they own the resource.
- Using UUIDs and believing that makes IDOR impossible — obscurity without authorisation checks is not security.
- Only protecting read endpoints and forgetting update and delete routes.
Code Examples
// No ownership check — any user can view any order
public function show(int $orderId): JsonResponse {
return response()->json(Order::findOrFail($orderId));
}
public function show(int $orderId): JsonResponse {
$order = Order::findOrFail($orderId);
// Enforce ownership — compare to authenticated user
if ($order->user_id !== auth()->id()) {
abort(403); // Forbidden — not just 404
}
return response()->json($order);
}
// Or scope query to current user (prevents the object being fetched at all)
$order = auth()->user()->orders()->findOrFail($orderId);