All articles
Vulnerabilities10 min readJanuary 9, 2026
IDORAuthorizationAccess ControlData Exposure

IDOR Vulnerabilities in AI Apps: When Users Access Each Other's Data

Insecure Direct Object References let users access data they shouldn't. Here's how AI creates them and how to fix them.

Security Guide

What Is IDOR?

Insecure Direct Object Reference (IDOR) happens when an application exposes internal object identifiers to users without proper authorization checks.

GET /api/invoices/123  β†’ User's invoice
GET /api/invoices/124  β†’ Someone else's invoice (!)

If changing the ID gives you access to another user's data, that's IDOR.

Why AI Creates IDOR Vulnerabilities

AI generates functional code. It creates endpoints that workβ€”but "working" doesn't mean "secure."

AI sees:

  • Request comes in with ID
  • Fetch data with that ID
  • Return data
AI doesn't consider:
  • Does this user own this resource?
  • Should they be able to see it?
  • What about delete/update operations?

Common IDOR Patterns

Pattern 1: Simple Object Access

AI-Generated (Vulnerable):

javascript
// GET /api/orders/:id
export async function GET(req, { params }) {
  const order = await db.orders.findUnique({
    where: { id: params.id }
  })
  return Response.json(order)
}

The Attack:

bash
# User finds their order ID: 42
curl /api/orders/42  # βœ“ Their order

# User tries other IDs curl /api/orders/1 # 😱 Admin's order curl /api/orders/43 # 😱 Another user's order

The Fix:

javascript
export async function GET(req, { params }) {
  const session = await getServerSession()

const order = await db.orders.findFirst({ where: { id: params.id, userId: session.user.id // ← Ownership check } })

if (!order) { return Response.json({ error: 'Not found' }, { status: 404 }) }

return Response.json(order) }

Pattern 2: Update/Delete Without Ownership Check

AI-Generated (Vulnerable):

javascript
// DELETE /api/posts/:id
export async function DELETE(req, { params }) {
  await db.posts.delete({ where: { id: params.id } })
  return Response.json({ success: true })
}

The Attack:

bash
# User deletes anyone's posts
curl -X DELETE /api/posts/999

The Fix:

javascript
export async function DELETE(req, { params }) {
  const session = await getServerSession()

const post = await db.posts.findFirst({ where: { id: params.id, authorId: session.user.id } })

if (!post) { return Response.json({ error: 'Not found' }, { status: 404 }) }

await db.posts.delete({ where: { id: params.id } }) return Response.json({ success: true }) }

Pattern 3: Batch Operations

AI-Generated (Vulnerable):

javascript
// POST /api/orders/export
export async function POST(req) {
  const { orderIds } = await req.json()

const orders = await db.orders.findMany({ where: { id: { in: orderIds } } })

return generateCSV(orders) }

The Attack:

javascript
// Request export of all orders
fetch('/api/orders/export', {
  method: 'POST',
  body: JSON.stringify({ orderIds: [1, 2, 3, 4, 5, /* ... */] })
})

The Fix:

javascript
export async function POST(req) {
  const session = await getServerSession()
  const { orderIds } = await req.json()

const orders = await db.orders.findMany({ where: { id: { in: orderIds }, userId: session.user.id // ← Filter to user's orders } })

return generateCSV(orders) }

Pattern 4: File Access

AI-Generated (Vulnerable):

javascript
// GET /api/files/:filename
export async function GET(req, { params }) {
  const file = await readFile(uploads/${params.filename})
  return new Response(file)
}

The Attack:

bash
# User accesses others' files
curl /api/files/user1_tax_return.pdf
curl /api/files/user2_medical_records.pdf

The Fix:

javascript
export async function GET(req, { params }) {
  const session = await getServerSession()

// Lookup file ownership in database const fileRecord = await db.files.findFirst({ where: { filename: params.filename, userId: session.user.id } })

if (!fileRecord) { return Response.json({ error: 'Not found' }, { status: 404 }) }

const file = await readFile(uploads/${params.filename}) return new Response(file) }

Pattern 5: URL Parameter Reference

AI-Generated (Vulnerable):

javascript
// GET /api/users/:userId/settings
export async function GET(req, { params }) {
  const settings = await db.userSettings.findUnique({
    where: { userId: params.userId }
  })
  return Response.json(settings)
}

The Attack:

bash
# User views others' settings
curl /api/users/admin/settings
curl /api/users/1/settings

The Fix:

javascript
export async function GET(req) {
  const session = await getServerSession()

// Use session user, not URL parameter const settings = await db.userSettings.findUnique({ where: { userId: session.user.id } })

return Response.json(settings) }

IDOR Prevention Strategies

Strategy 1: Always Check Ownership

javascript
// Utility function
async function authorizeResource(resourceType, resourceId, userId) {
  const resource = await db[resourceType].findFirst({
    where: {
      id: resourceId,
      userId: userId
    }
  })

if (!resource) { throw new AuthorizationError('Access denied') }

return resource }

// Usage const order = await authorizeResource('orders', params.id, session.user.id)

Strategy 2: Use UUIDs Instead of Sequential IDs

javascript
// Sequential IDs are easy to guess
/api/orders/1
/api/orders/2
/api/orders/3

// UUIDs are not /api/orders/550e8400-e29b-41d4-a716-446655440000

Note: UUIDs reduce guessability but don't replace authorization checks.

Strategy 3: Row-Level Security (Supabase/PostgreSQL)

sql
-- Enable RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- Policy: Users see only their orders CREATE POLICY "Users view own orders" ON orders FOR SELECT USING (auth.uid() = user_id);

-- Policy: Users update only their orders CREATE POLICY "Users update own orders" ON orders FOR UPDATE USING (auth.uid() = user_id);

Strategy 4: Indirect References

javascript
// Instead of:
/api/orders/123

// Use user-specific index: /api/my-orders/1 // User's first order /api/my-orders/2 // User's second order

export async function GET(req, { params }) { const session = await getServerSession()

const orders = await db.orders.findMany({ where: { userId: session.user.id }, orderBy: { createdAt: 'asc' } })

return Response.json(orders[params.index - 1]) }

Testing for IDOR

Manual Testing Steps

  1. Log in as User A
  2. Access a resource: /api/orders/42
  3. Note the response
  4. Log in as User B
  5. Try accessing User A's resource: /api/orders/42
  6. If you get User A's data β†’ IDOR vulnerability

Automated Testing

javascript
// Test script
const userAToken = 'token-for-user-a'
const userBToken = 'token-for-user-b'

// Create resource as User A const order = await fetch('/api/orders', { method: 'POST', headers: { Authorization: Bearer ${userAToken} }, body: JSON.stringify({ items: ['test'] }) }).then(r => r.json())

// Try to access as User B const accessAttempt = await fetch(/api/orders/${order.id}, { headers: { Authorization: Bearer ${userBToken} } })

// Should be 404 or 403, not 200 if (accessAttempt.status === 200) { console.error('IDOR VULNERABILITY FOUND!') }

IDOR Checklist

FOR EVERY ENDPOINT WITH AN ID PARAMETER:
========================================
[ ] Ownership verified before read
[ ] Ownership verified before update
[ ] Ownership verified before delete
[ ] Batch operations filter by user
[ ] File access checks ownership

ADDITIONAL PROTECTION: ===================== [ ] UUIDs instead of sequential IDs [ ] RLS enabled on database tables [ ] Authorization utility functions [ ] Automated IDOR testing in CI

The Bottom Line

IDOR is the "forgot to check ownership" vulnerability. AI always forgets. Every endpoint that takes an ID parameter needs an ownership check.

If the URL has an ID, verify the user should access it.

Ready to secure your AI-generated code?

Stop reading about vulnerabilities. Start fixing them.

Start Scanning Free