Startup Cybersecurity // 2026
CybersecurityApril 22, 2026·9 min read

IDOR Vulnerabilities in SaaS Apps: The Broken Access Control Bug That Breaches Startups

IDOR is not a legacy bug. It is the default startup failure mode when teams build features fast, trust record ids, and confuse authentication with authorization.

Why This Matters

IDOR is the shortest path from “the user is logged in” to “the attacker can read another customer's data.” The fix is not abstract security theory. It is a consistent rule: every record fetch, update, and delete must enforce ownership or tenant scope at the data boundary itself.

The reason IDOR keeps showing up in startup apps is simple: CRUD code is easy to scaffold, and authorization is easy to postpone. A route works in development the moment it can fetch a record by id. It only becomes a breach when someone asks what happens if the id belongs to another account.

This is why broken access control stays at the top of the OWASP list. Teams often think they have “auth” because there is a session token or a Clerk userId. But auth only answers who the user is. Authorization answers whether this user is allowed to touch this resource. That second question is where the incident lives.

1
Missing ownership check can expose every tenant
A01
OWASP category most often behind SaaS data leaks
0
Safe endpoints that trust route params alone

What IDOR Looks Like in Startup Code

Most IDOR bugs are visually boring. There is no fancy exploit chain. The route accepts an id, the database returns the matching row, and the response serializes it back to the caller. If the caller is authenticated, the code feels correct. It is only wrong because the resource itself is not scoped to the caller.

In SaaS products, the vulnerable surfaces are predictable: document views, invoices, reports, support tickets, file downloads, team settings, billing objects, invite links, export endpoints, and admin tools copied from internal use into the customer product. If the identifier can be guessed, logged, shared, or enumerated, the route needs explicit access control.

UUIDs help with unpredictability. They do not solve authorization. A UUID is still an object reference. If your code treats possession of the id as permission, the route is still insecure.

Where SaaS Teams Most Often Hide IDOR

These are the feature types that deserve immediate review in almost every startup codebase.

High Risk

Document and report endpoints

Anything under /api/reports/[id], /files/[id], or /documents/[id] is high risk because the entire resource is chosen by a caller-controlled parameter.

High Risk

Team settings and invites

A route that updates a team by id without verifying team membership becomes cross-customer account control, not just read leakage.

Revenue

Billing and subscription objects

Customer ids, invoice ids, and checkout session references often get passed around in dashboards and admin views with weaker auth assumptions.

Exports

Background export downloads

Signed URLs and download jobs sometimes validate only that the job exists, not that the requester belongs to the job owner or tenant.

Ops

Support or admin tools reused in prod

Internal tools are often built with implicit trust. When those code paths get exposed to customer-facing workflows, the authz gap comes with them.

The Exact Pattern to Delete From Your Codebase

Never fetch or mutate a customer object by id alone if the caller is not a fully trusted internal service.

IDOR Pattern
export async function PATCH(
  req: Request,
  { params }: { params: { id: string } }
) {
  const { userId } = await auth();
  if (!userId) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const body = await req.json();

  await db.update(projects)
    .set(body)
    .where(eq(projects.id, params.id));

  return Response.json({ ok: true });
}
Tenant-Safe Pattern
export async function PATCH(
  req: Request,
  { params }: { params: { id: string } }
) {
  const { userId } = await auth();
  if (!userId) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const membership = await db.query.memberships.findFirst({
    where: eq(memberships.userId, userId),
  });

  if (!membership) {
    return Response.json({ error: 'Forbidden' }, { status: 403 });
  }

  await db.update(projects)
    .set(await ProjectUpdateSchema.parseAsync(await req.json()))
    .where(and(
      eq(projects.id, params.id),
      eq(projects.tenantId, membership.tenantId)
    ));

  return Response.json({ ok: true });
}

The fix is stronger than “if project.ownerId === userId.” In multi-tenant systems, the correct boundary is often team or tenant membership, not just a single owner field.

If the access rule is complex, centralize it in a helper or repository layer. The goal is to make the safe query the easiest query.

How to Prevent IDOR Systematically

Scope every query by tenant or owner

Query Design

Do not perform a wide read and then filter in application memory. That leaks through timing, logging, and race conditions even when the final response is blocked.

Separate admin-only paths from customer paths

Privilege

If a route is allowed to see any tenant's record, it should live behind an explicit admin guard and not be reused casually in customer features.

Test with cross-account fixtures

Testing

Every feature test should include “user A tries to access user B's resource” for reads, writes, deletes, downloads, and exports.

Do not rely on UUID secrecy

Design

Unpredictable ids reduce casual abuse but do not replace authorization logic. Treat ids as routing hints, not permissions.

Scan for routes touching params.id without access checks

Automation

This is where static analysis helps. The pattern appears repeatedly across handlers and action code when teams move fast.

Find IDOR Before Attackers Do

Scan Every Resource Path for Authorization Gaps

Custodia highlights the route handlers and mutations most likely to leak cross-account data so you can fix them before the first enterprise customer asks hard questions.

// npx custodia-cli scan
$ npx custodia-cli scan

  ┌──────────────────────────────────────────────────────┐
  │  CUSTODIA.DEV  //  STARTUP SECURITY ANALYSIS         │
  └──────────────────────────────────────────────────────┘

  CRITICAL AUTH-07 IDOR on report download
          src/app/api/reports/[id]/download/route.ts:24
          Download link verifies login state but not report ownership or tenant membership.

  HIGH     AUTH-07 Unsafe invoice lookup
          src/app/api/invoices/[id]/route.ts:19
          Invoice returned by id alone from a multi-tenant table.

  MEDIUM   AUTH-03 Weak admin utility separation
          src/app/api/internal/projects/[id]/route.ts:14
          Internal maintenance path exposed without explicit admin role enforcement.

  ───────────────────────────────────────────────────────
  OUTPUT: file-level findings, fix guidance, severity map
  COVERAGE: auth, secrets, injection, access control, AI
Scan My CodebaseView Demo Report

Frequently Asked Questions

What is an IDOR vulnerability?

IDOR stands for insecure direct object reference. It happens when an application uses a user-supplied identifier to access a resource without verifying that the current user is allowed to access that resource.

Are UUIDs enough to prevent IDOR?

No. UUIDs make guessing harder, but they do not enforce authorization. If the application returns a record whenever the id exists, the route is still vulnerable.

Is IDOR only about reads?

No. Updates, deletes, downloads, exports, and admin actions are often more dangerous than reads because they let an attacker alter or destroy other customers' data.

What is the safest fix for IDOR?

Enforce the access boundary in the database query or repository method itself. Safe code reads “record with this id that belongs to this user or tenant,” not just “record with this id.”

Can static analysis catch IDOR?

It can catch many high-signal patterns, especially handlers that fetch by route parameter and mutate or return data without a nearby authz check. It is one of the most valuable categories to scan before launch.

Related Articles
CybersecurityMulti-Tenant Isolation Security for SaaS StartupsCybersecurityNext.js Security Checklist for StartupsCybersecurityOWASP Top 10 Code Review Guide