How to Avoid: IDOR — Resource Fetched by ID Without Ownership Check
July 20, 2026 · 8-minute read · Fairy
The short answer
To prevent IDOR (Insecure Direct Object Reference) in AI-generated code, scope every resource lookup to the authenticated user. Instead of `SELECT * FROM orders WHERE id = ?`, use `SELECT * FROM orders WHERE id = ? AND user_id = ?`. AI models frequently omit ownership checks because training data often shows simplified examples without multi-tenant context.
The Direct Answer: Scope Every Query to the Authenticated User
To prevent IDOR (Insecure Direct Object Reference) vulnerabilities in AI-generated code, add an ownership check to every resource lookup. When your code fetches a resource by ID, the query must also verify that the authenticated user has permission to access that specific resource.
The vulnerable pattern:
// VULNERABLE: AI-generated code that fetches by ID alone
app.get('/api/orders/:id', authenticate, async (req, res) => {
const order = await db.orders.findOne({ id: req.params.id });
return res.json(order);
});
The secure pattern:
// SECURE: Query scoped to the authenticated user
app.get('/api/orders/:id', authenticate, async (req, res) => {
const order = await db.orders.findOne({
id: req.params.id,
userId: req.user.id // Ownership check
});
if (!order) return res.status(404).json({ error: 'Not found' });
return res.json(order);
});
This single change—adding userId: req.user.id to the query—prevents attackers from accessing other users' data by manipulating the ID parameter.
What Is IDOR and Why Is It Dangerous?
IDOR (Insecure Direct Object Reference) is an access control vulnerability where an application uses user-supplied input to directly access objects without verifying permission. When your API endpoint accepts /orders/123 and returns order 123 without checking if the requesting user owns that order, anyone can enumerate IDs and access arbitrary records.
The consequences are severe:
- Data exposure: Attackers access other users' personal information, financial records, or private documents
- Data modification: Attackers can update or delete resources they don't own
- Compliance violations: Unauthorized data access triggers GDPR, HIPAA, and SOC 2 failures
- Privilege escalation: Attackers may access admin resources by guessing admin IDs
IDOR consistently ranks in the OWASP Top 10 under "Broken Access Control" because it's common, easy to exploit, and often catastrophic. Unlike injection attacks that require crafted payloads, IDOR exploitation is as simple as changing a number in a URL.
Why AI Generates Code With IDOR Vulnerabilities
AI code generators produce IDOR vulnerabilities for predictable reasons rooted in how they learn and generate code.
Training Data Shows Simplified Examples
Most code in training datasets comes from tutorials, documentation, and example projects. These sources demonstrate functionality—how to fetch a record by ID—not production security patterns. A tutorial showing CRUD operations doesn't include multi-tenant authorization because it's teaching database queries, not access control.
When you prompt an AI to "create an endpoint to get an order by ID," it generates what it learned: a query that gets an order by ID. The ownership requirement is implicit in your mental model but absent from the prompt.
Context Window Limitations
AI models don't automatically carry forward that your application is multi-tenant, that users should only see their own data, or that the user_id column exists for access control purposes. Each generation treats the immediate request in relative isolation.
Even when your codebase has ownership checks elsewhere, the AI may not consistently apply the same pattern to new endpoints unless explicitly instructed.
Functionality Over Security
AI models optimize for generating code that works—code that compiles, runs, and produces the expected output. An endpoint that fetches any order by ID is functionally correct. It returns the requested order. The security requirement that it should only return orders belonging to the authenticated user is a constraint the AI doesn't infer.
This pattern appears across security domains. Fairy's code reviews frequently identify related issues: protected routes missing auth checks where authentication middleware doesn't cover all endpoints, and database configurations where row-level security isn't enabled.
The Secure Pattern: Ownership Checks in Practice
The fix is consistent: every query that fetches a resource by user-supplied ID must also constrain by the authenticated user's identity.
SQL Queries
-- VULNERABLE
SELECT * FROM documents WHERE id = $1;
-- SECURE
SELECT * FROM documents WHERE id = $1 AND user_id = $2;
ORM Queries
// VULNERABLE: Prisma without ownership check
const invoice = await prisma.invoice.findUnique({
where: { id: invoiceId }
});
// SECURE: Prisma with ownership check
const invoice = await prisma.invoice.findFirst({
where: {
id: invoiceId,
userId: req.user.id
}
});
Note the change from findUnique to findFirst. With findUnique, you can only query by unique fields. Adding userId to the where clause requires findFirst since the combination isn't a unique constraint.
MongoDB Queries
// VULNERABLE
const record = await collection.findOne({ _id: ObjectId(id) });
// SECURE
const record = await collection.findOne({
_id: ObjectId(id),
userId: req.user.id
});
Handling Not Found vs. Forbidden
When a resource doesn't exist or the user doesn't own it, return 404 Not Found rather than 403 Forbidden. Returning 403 confirms the resource exists, which leaks information to attackers probing for valid IDs.
const resource = await db.resources.findOne({
id: req.params.id,
userId: req.user.id
});
if (!resource) {
// Don't distinguish between "doesn't exist" and "not yours"
return res.status(404).json({ error: 'Resource not found' });
}
Database-Level Enforcement: Row-Level Security
Ownership checks in application code are necessary, but defense in depth means enforcing access control at multiple layers. Row-Level Security (RLS) in PostgreSQL provides database-level enforcement.
-- Enable RLS on the table
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Create policy that restricts access to owned rows
CREATE POLICY user_orders ON orders
FOR ALL
USING (user_id = current_setting('app.current_user_id')::uuid);
With RLS enabled, even if application code forgets the ownership check, the database won't return rows the user doesn't own.
This is particularly important for platforms like Supabase, where AI-generated code frequently creates tables without enabling RLS—leaving every row readable and writable by any authenticated user. When using Fairy for Code, these database configuration gaps are flagged alongside application-level IDOR vulnerabilities.
Detection: How to Find IDOR in Your Codebase
Code Review Signals
Search for these patterns that indicate potential IDOR:
# Queries that fetch by ID without user constraint
findOne.*id:.*params
findUnique.*where:.*id
SELECT.*WHERE.*id = \$
findById\(
# Endpoints with ID parameters
app.get.*/:id
router.get.*/:.*Id
@Get(':id')
Review each match to verify an ownership check is present.
Automated Testing
Write tests that verify access control:
describe('IDOR Protection', () => {
it('should not allow access to other users orders', async () => {
// Create order as user A
const orderA = await createOrder(userA.token, { item: 'test' });
// Attempt to access as user B
const response = await request(app)
.get(`/api/orders/${orderA.id}`)
.set('Authorization', `Bearer ${userB.token}`);
expect(response.status).toBe(404);
});
});
Manual Testing Checklist
- Create two test user accounts
- As User A, create resources and note their IDs
- As User B, attempt to GET, PUT, PATCH, and DELETE User A's resources
- Verify User B receives 404 for all operations
- Check that responses don't leak information about whether the resource exists
Common Variations of IDOR in AI-Generated Code
Nested Resource IDOR
// VULNERABLE: Checks order ownership but not item ownership
app.get('/orders/:orderId/items/:itemId', authenticate, async (req, res) => {
const order = await db.orders.findOne({
id: req.params.orderId,
userId: req.user.id // Good: checks order ownership
});
if (!order) return res.status(404).send();
// Bad: doesn't verify item belongs to this order
const item = await db.orderItems.findOne({ id: req.params.itemId });
return res.json(item);
});
// SECURE: Verify entire chain
const item = await db.orderItems.findOne({
id: req.params.itemId,
orderId: req.params.orderId // Verify item belongs to order
});
Bulk Operations IDOR
// VULNERABLE: Accepts array of IDs without checking ownership
app.post('/api/documents/bulk-delete', authenticate, async (req, res) => {
await db.documents.deleteMany({ id: { $in: req.body.ids } });
return res.json({ deleted: req.body.ids.length });
});
// SECURE: Scope deletion to owned documents only
app.post('/api/documents/bulk-delete', authenticate, async (req, res) => {
const result = await db.documents.deleteMany({
id: { $in: req.body.ids },
userId: req.user.id
});
return res.json({ deleted: result.deletedCount });
});
File Upload/Download IDOR
// VULNERABLE: File path derived from user input without ownership check
app.get('/files/:fileId', authenticate, async (req, res) => {
const file = await db.files.findOne({ id: req.params.fileId });
return res.sendFile(file.path);
});
// SECURE: Verify file ownership before serving
app.get('/files/:fileId', authenticate, async (req, res) => {
const file = await db.files.findOne({
id: req.params.fileId,
uploadedBy: req.user.id
});
if (!file) return res.status(404).send();
return res.sendFile(file.path);
});
Integrating IDOR Prevention Into AI-Assisted Development
Prompt Engineering
When prompting AI to generate endpoints, explicitly state the security requirement:
Create a GET endpoint for /api/invoices/:id that:
1. Requires authentication
2. Fetches the invoice by ID AND verifies it belongs to the authenticated user
3. Returns 404 if not found or not owned by the user
Pre-Commit Verification
Before committing AI-generated code, verify every endpoint that accepts an ID parameter includes an ownership check. This is where Fairy Scout, our free PR review tool, provides immediate value—catching authorization gaps before they reach production.
Architectural Guardrails
Consider a centralized authorization layer that all resource access flows through:
class ResourceAuthorizer {
async authorize(resourceType, resourceId, userId, action = 'read') {
const resource = await this.db[resourceType].findOne({
id: resourceId,
userId: userId
});
if (!resource) {
throw new NotFoundError(`${resourceType} not found`);
}
return resource;
}
}
// Usage in endpoints
app.get('/api/orders/:id', authenticate, async (req, res) => {
const order = await authorizer.authorize('orders', req.params.id, req.user.id);
return res.json(order);
});
This pattern makes it harder to forget ownership checks—the authorization is built into the data access layer.
Beyond IDOR: The Authorization Stack
IDOR is one component of a broader authorization challenge. AI-generated code frequently exhibits related vulnerabilities:
- Missing authentication on protected routes: The route exists but doesn't require a logged-in user
- JWT tokens without expiration: Leaked tokens remain valid indefinitely
- Role checks that can be bypassed: Admin functionality accessible to regular users
These issues share a common cause: AI generates code that works functionally without inferring implicit security requirements. The solution is consistent review of all AI-generated code that touches authentication, authorization, or resource access.
For teams deploying AI-generated code in production, Fairy provides the verification layer that catches these authorization gaps systematically—before they become breaches.
Frequently asked questions
What is an IDOR vulnerability?
IDOR (Insecure Direct Object Reference) occurs when an application exposes internal object IDs in URLs or parameters and fetches resources without verifying the requesting user owns or has permission to access them. Attackers can manipulate IDs to access other users' data.
Why does AI-generated code often have IDOR bugs?
AI models learn from training data that frequently shows single-user examples or tutorials without multi-tenant authorization logic. The models optimize for functionality—fetching the requested resource—without inferring the implicit security requirement that access should be scoped to the authenticated user.
How do I test for IDOR in my API endpoints?
Create two test accounts. With Account A, note a resource ID. Then authenticate as Account B and attempt to access that resource ID. If Account B can view or modify Account A's resource, you have an IDOR vulnerability.
Is checking authentication enough to prevent IDOR?
No. Authentication confirms who the user is; authorization confirms what they can access. An authenticated user can still access resources they don't own if your code only checks that they're logged in without verifying ownership of the specific resource.
What's the difference between IDOR and broken access control?
IDOR is a specific type of broken access control. Broken access control is the broader category covering any failure to enforce proper permissions. IDOR specifically refers to accessing resources by manipulating direct object references like IDs in URLs or request bodies.
Have AI-generated work you’d want verified? Connect with a Fairy → or run a free check with Scout.
More resources