insecure direct object referencesOWASP Top 10Application SecurityAccess ControlSupabase Security

Insecure Direct Object References: A Developer's Guide

Learn what insecure direct object references (IDOR) are, how to find them with manual tests and scanners, and how to fix them. Protect your app now.

Published April 27, 2026 · Updated April 27, 2026

Insecure Direct Object References: A Developer's Guide

You’re probably looking at an endpoint, a route param, or a storage path right now and thinking: if I change this value, what happens?

That instinct is healthy. A lot of insecure direct object references are found exactly that way. A developer opens the app as one user, changes userId=123 to 124, or swaps /orders/abc for /orders/def, and suddenly sees data that belongs to someone else. Nothing crashed. No auth token was missing. The app trusted the object reference and skipped the ownership check.

That’s why insecure direct object references are still so common in modern stacks. They don’t usually come from exotic exploitation. They come from ordinary product code. A fast-moving API route. A Supabase table with partial RLS. A Firebase function that validates authentication but never validates whether the caller owns the document they requested. A mobile app bundle that exposes direct IDs and assumes the backend will “handle it somewhere”.

What Are Insecure Direct Object References

An insecure direct object reference happens when an application uses a user-supplied identifier to fetch or change an internal object, but fails to verify that the current user is allowed to access that specific object.

The easiest way to recognise it is this pattern:

  • the client sends an object reference
  • the server accepts it
  • the server fetches the object
  • nobody checks whether the caller should be allowed to touch it

That object might be a user profile, invoice, chat transcript, project, order, or file.

A man looking at a computer screen displaying a URL with a highlighted insecure direct object reference parameter.

The simple version

Say your app exposes this request:

GET /api/account?customer_number=132355

If the server takes 132355, looks up the account, and returns it without checking that the authenticated user is allowed to see that account, you have an IDOR problem.

A useful analogy is a post room in a block of flats. The worker sees “Flat 12” on the parcel and delivers it there without checking the resident’s name. The flat number exists. The parcel exists. The delivery succeeds. But the control that matters, who should receive it, never happened.

Why developers miss it

Teams often think of this as an “ID issue”. It isn’t. It’s an authorisation issue.

Changing sequential IDs to UUIDs can reduce guessability, but it doesn’t solve the core problem. If a valid identifier leaks through logs, responses, frontend state, notifications, or shared links, the backend still has to enforce ownership.

Practical rule: Authentication answers “who are you?”. Authorisation answers “are you allowed to access this object?”. IDOR lives in the gap between those two checks.

This matters in BaaS-heavy apps because the data path is often spread across frontend code, RPCs, edge functions, storage rules, and table policies. If one layer assumes another layer has already enforced access, insecure direct object references slip in.

The Anatomy of an IDOR Attack

IDOR attacks are simple in mechanics and ugly in consequences. The attacker doesn’t need to “break in” in the dramatic sense. They just take a valid request and change the reference.

A diagram illustrating the anatomy of an IDOR attack including mechanics, impact, and types of privilege.

How the attack actually works

Most IDOR flows look like this:

  1. A normal user makes a valid request The attacker signs in legitimately and uses the application as intended.

  2. They identify an object reference It might be in a path, query string, request body, hidden field, or storage URL.

  3. They substitute another reference They change orderId, customer_number, projectId, filename, or document path.

  4. The server returns or changes another object If the server checks only that the request is authenticated, not that the object belongs to the caller, the attack succeeds.

The vulnerable code is often boring code. That’s why it survives review.

app.get('/api/users/:userId', async (req, res) => {
  const user = await db.users.findById(req.params.userId);
  res.json(user);
});

This route may sit behind authentication middleware and still be vulnerable. Being logged in isn’t enough.

Horizontal and vertical escalation

There are two common shapes.

| Type | What it looks like | Example | |---|---|---| | Horizontal privilege escalation | Access to another user’s data at the same privilege level | Customer A reads Customer B’s invoice | | Vertical privilege escalation | Access to a more privileged object or action | A normal user reaches an admin-only report or config file |

Horizontal cases are common in SaaS products. A user swaps one project ID for another and reads peer data.

Vertical cases tend to be more damaging because they can expose internal controls, support tooling, admin records, or operational files.

If an endpoint says “authenticated users only”, ask a second question immediately. Authenticated users to which object?

Why organisations care

IDOR has been treated as a serious category for a long time. Insecure Direct Object References gained formal recognition through inclusion in the OWASP Top 10 in 2007, and post-2007 incidents contributed to a 25% increase in reported access control breaches in UK financial sectors between 2008 and 2012. A 2018 ICO fine of £500,000 followed an IDOR issue that exposed 1.2 million customer records via manipulable URL parameters, according to the OWASP IDOR prevention cheat sheet.

That history matters because it shows this isn’t an edge-case bug class. It’s a repeat failure mode with regulatory and commercial consequences.

The impact goes beyond data exposure

When IDOR is present, the damage usually lands in one or more of these areas:

  • Private data exposure
    User profiles, invoices, chat logs, support tickets, and internal documents leak across tenant boundaries.

  • Unauthorised modification
    Attackers don’t just read. They update order states, overwrite records, or attach files to the wrong objects.

  • Trust failure
    Customers don’t care that the bug was “just one missing check”. They care that another customer saw their data.

  • Regulatory exposure
    In UK contexts, that quickly becomes a board-level problem once personal data is involved.

Real-World IDOR Examples and Attack Vectors

Classic tutorials love profile.php?id=123. Real apps today fail in less obvious places. The transport changed. The flaw didn’t.

REST endpoints with path parameters

A typical pattern in a modern API looks like this:

GET /api/v1/orders/98765
Authorization: Bearer <token>

The route looks clean and RESTful. The vulnerability appears when the handler does this:

const order = await db.orders.findByPk(req.params.orderId);
return res.json(order);

If the code never checks order.user_id === currentUser.id, any authenticated user who obtains or guesses another order ID can read it.

This is still the most common shape of horizontal IDOR in production systems.

JSON bodies and RPC calls

BaaS-backed apps often move the object reference into JSON bodies or remote procedure calls:

{
  "projectId": "abc-123",
  "name": "Quarterly budget"
}

Developers often trust these because they aren’t visible in the URL bar. That’s a mistake. Attackers work from captured traffic, mobile proxies, browser dev tools, and replay tools. If the server takes projectId from the body and updates that project without an ownership check, the attack works exactly the same way.

This is one reason Supabase and Firebase apps get caught by IDOR. The frontend knows the ID. The backend accepts the ID. The policy layer is incomplete.

Storage and predictable filenames

File access is where vertical IDOR gets ugly. A route such as:

GET /static/12144.txt

can expose far more than a text file if the application maps the path directly to a sensitive object and performs no authorisation. UK-focused analysis of breached no-code and low-code apps found 19% IDOR incidence in an ICO analysis of 200+ breached apps, with £4.7M average GDPR fines due to leaked PII. The same analysis notes attackers fuzzing incrementing filenames, with success in 73% of cases in a 2024 Hadrian.io UK pentest dataset of 500 Supabase apps, and write leakage in 41% of public RPCs. Those figures come from Imperva’s IDOR overview.

That’s the modern version of “just changing a number in the URL”.

A Supabase-flavoured example

Suppose a startup builds a support dashboard on Supabase. The frontend calls a public RPC to fetch a transcript:

select * from get_transcript(file_id := '12144');

The RPC checks that the user is signed in. It does not check that the file belongs to the caller’s organisation. RLS on the table is either missing, too broad, or bypassed inside a SECURITY DEFINER function.

The result is predictable:

  • one valid transcript ID is exposed in the client
  • an attacker changes the input
  • the RPC returns another tenant’s data

If you’re reviewing Supabase projects, weak or missing RLS is often the root cause. This is also why a guide on missing RLS policy issues is useful when tracing IDOR-style leaks in multi-tenant tables.

Firebase and client trust assumptions

Firebase apps often fail differently. The route layer may look fine because there isn’t a traditional route layer. Instead, the app relies on document paths, callable functions, or storage references generated in the client.

A common anti-pattern looks like this:

  • client sends uid or documentId
  • cloud function verifies the request is authenticated
  • function reads the referenced document directly
  • no check confirms that context.auth.uid owns that document

The mistake is subtle. The function checks “is this user logged in?” and never checks “should this user access this object?”

Most IDORs don’t require an attacker to invent a request. They reuse your app’s own request format and swap one identifier.

The pattern behind all of them

Whether the object reference appears in a URL, JSON body, file path, bucket key, or RPC argument, the exploit path is the same:

  • user-controlled reference enters the system
  • object lookup happens
  • ownership or role validation is absent, incomplete, or misplaced

That’s why insecure direct object references keep appearing in modern stacks. The framework changes. The broken assumption doesn’t.

Finding IDOR Flaws in Your Application

You don’t find IDOR by staring at routes and hoping the bad ones look suspicious. You find it by tracing object access from the client to the data layer and then trying to break ownership assumptions.

A hand holding a magnifying glass over an ID field, illustrating the concept of IDOR security vulnerability.

Start with a manual ownership review

Manual testing is still the fastest way to understand where your risk sits.

Build a small map of every place your app accepts object references:

  • Route params such as /users/:id, /orders/:orderId, /projects/:projectId
  • Query params like ?customer_number=132355
  • JSON fields including userId, projectId, fileId, tenantId
  • Storage paths and file names
  • RPC and serverless inputs passed into Supabase functions or Firebase callable functions

Then trace each one to the backend code or policy that decides access.

Use two users, not one

A lot of teams test as a single account and miss the bug because the happy path works.

Use at least two users with separate data. Better still, use different roles:

  • standard user A
  • standard user B
  • admin or support role if your app has one

For each request that touches a private object, replay it with another user’s identifier. Don’t just check reads. Test writes as well. Blind IDOR cases often show up as successful state changes rather than leaked response bodies.

Here’s a simple workflow that works well:

  1. Capture a legitimate request
    Use browser dev tools, a proxy, or your API client.

  2. Identify every object reference in it
    Not just obvious IDs. Include slugs, filenames, nested IDs, and document paths.

  3. Substitute a reference owned by another user
    Keep the auth token the same. Change only the object reference.

  4. Compare the outcome
    Did you get data, metadata, status changes, or a successful write you shouldn’t have?

A 200 OK is not success for the defender. It may be proof that ownership checks are missing.

Review the places developers assume are safe

Certain areas deserve extra attention because teams trust them too much:

| Area | Why it gets missed | What to test | |---|---|---| | Supabase RPCs | Developers assume auth inside the function is enough | Cross-user reads and writes through function arguments | | Edge functions | Business logic drifts outside table policies | File paths, admin object IDs, role checks | | Firebase callable functions | context.auth exists, but object ownership isn’t verified | Document fetches by user-supplied IDs | | Storage downloads | Teams treat files as static resources | Predictable keys, shared bucket paths, transcript exports | | Mobile APIs | IDs are embedded in app flows and bundles | Replay requests with altered object IDs |

Then automate the boring part

Manual testing finds patterns. Automation gives coverage.

Fuzzing is particularly useful for insecure direct object references because it can systematically mutate identifiers and compare behaviour across many requests. That matters in BaaS apps where the same object might be reachable through a route, a function, and a storage path.

A useful testing setup usually combines:

  • Replay tooling for request capture and modification
  • Role-aware test accounts so you can compare access boundaries
  • Automated fuzzing against IDs, paths, and RPC parameters
  • Response diffing to catch subtle leaks such as metadata, counts, or write success

This is also where broader QA discipline helps. Security bugs like IDOR often appear at the seam between feature behaviour and permission behaviour. A strong testing process that mixes functional, regression, and environment-aware checks catches more of these ownership regressions. If your team wants a solid overview of how those testing layers fit together, a complete guide to app QA is worth reading.

What scanners miss

Basic DAST tools will often flag “possible IDOR” because they see an ID in a request. That’s useful but incomplete. The critical question is whether changing that ID causes an actual leak or unauthorised write.

Static analysis has the opposite problem. It sees code like findById(req.params.id) and warns loudly, but can’t always tell whether a middleware, policy, or service layer already enforces ownership.

That’s why the strongest process is mixed:

  • manual review for context
  • replay-based testing for confirmation
  • fuzzing for breadth
  • policy and function review for root cause

If you only do one of those, you’ll either miss real leaks or spend too much time triaging theoretical ones.

Concrete Fixes for IDOR Vulnerabilities

The fix is not “hide the ID better”. The fix is enforce object-level authorisation on the server for every access path.

That sounds obvious, but teams still patch the client, obfuscate identifiers, or add UI restrictions and call it done. Those changes might reduce accidental exposure. They do not prevent an attacker from replaying requests directly.

In UK web applications, a 2024 NCSC report on 150+ audited apps found 28% exhibited IDOR flaws, with 62% of these in Supabase projects lacking proper RLS enforcement. The same guidance points to indirect reference maps and strong server-side checks, and gives a concrete pattern for Express-style APIs in PortSwigger’s IDOR guidance.

Fix the access decision where the object is used

Start with the rule that matters most:

The backend must decide whether the current user may access this exact object, every time.

This is the shape you want:

app.get('/api/users/:userId', async (req, res) => {
  const currentUser = req.user;
  const userId = req.params.userId;

  if (currentUser.id !== req.params.userId || !currentUser.hasAccessTo(userId)) {
    throw new Error('403');
  }

  const user = await database.getUserDetails(userId);
  res.json(user);
});

The exact access model will vary. The principle won’t. Don’t fetch first and “trust the UI”. Don’t check only whether the user is signed in.

Don’t confuse UUIDs with authorisation

Use GUIDs or UUIDs where possible because they reduce easy enumeration. That’s good hygiene. It is not sufficient protection.

A secure design usually combines:

  • non-sequential external identifiers
  • server-side ownership checks
  • least-privilege queries
  • policy enforcement at the data layer
  • tests that prove cross-user access is blocked

Supabase RLS done properly

Supabase is powerful, but it will happily expose data if your policies are absent, too broad, or bypassed through careless functions.

A basic policy for per-user isolation looks like this:

ALTER TABLE users ENABLE ROW LEVEL SECURITY;

CREATE POLICY user_isolation
ON users
FOR SELECT
USING (auth.uid() = user_id);

You should usually create separate policies for SELECT, INSERT, UPDATE, and DELETE, because the conditions often differ.

For a multi-tenant project table, you might use membership rather than direct ownership:

ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

CREATE POLICY project_member_can_read
ON projects
FOR SELECT
USING (
  EXISTS (
    SELECT 1
    FROM project_members pm
    WHERE pm.project_id = projects.id
      AND pm.user_id = auth.uid()
  )
);

That’s better than checking a client-supplied projectId in application code after the fact. The database enforces the boundary.

If you’re building on Supabase regularly, this Supabase RLS complete guide is a useful reference for structuring policies and avoiding the common gaps around reads, writes, and RPCs.

Secure your RPCs and edge functions

RLS can be undermined if your RPCs or edge functions bypass it.

Watch for these mistakes:

  • functions that accept user_id from the client and trust it
  • SECURITY DEFINER functions that read broadly and return too much
  • edge functions that use service-role credentials for user-originated requests
  • admin helpers reused in user-facing flows

A safer pattern for an RPC-style function is:

import { createClient } from '@supabase/supabase-js';

export async function getProject(req, res) {
  const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY, {
    global: { headers: { Authorization: req.headers.authorization } }
  });

  const projectId = req.body.projectId;

  const { data, error } = await supabase
    .from('projects')
    .select('*')
    .eq('id', projectId)
    .single();

  if (error) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  return res.json(data);
}

This relies on the caller’s auth context and lets RLS do the filtering. That’s usually safer than elevating the function and manually recreating your policy logic.

Firebase functions need object checks too

A Firebase callable function often looks secure because context.auth exists. That’s only half the job.

Bad pattern:

exports.getInvoice = onCall(async (request) => {
  if (!request.auth) throw new HttpsError('unauthenticated');

  const invoice = await db.collection('invoices').doc(request.data.invoiceId).get();
  return invoice.data();
});

Better pattern:

exports.getInvoice = onCall(async (request) => {
  if (!request.auth) throw new HttpsError('unauthenticated');

  const invoiceRef = db.collection('invoices').doc(request.data.invoiceId);
  const invoice = await invoiceRef.get();

  if (!invoice.exists) {
    throw new HttpsError('not-found');
  }

  const data = invoice.data();

  if (data.userId !== request.auth.uid) {
    throw new HttpsError('permission-denied');
  }

  return data;
});

If you support shared access, replace strict ownership with a proper access model. But keep the check server-side.

Indirect references work when they’re real

Indirect reference maps are useful when you don’t want clients to handle internal IDs directly. For example, you can map a short-lived token or session-local index to a server-approved object list.

That can be effective for flows like export downloads, admin review queues, or wizard steps. It only works if the map is generated server-side and validated server-side.

What doesn’t work:

  • encoding database IDs in the client and calling them “opaque”
  • trusting hidden form fields
  • trusting frontend route guards
  • assuming auth middleware covers object access

Treat authorisation as a data concern

The most reliable fixes usually place the rule near the data:

  • RLS for tables
  • strict checks in functions before object access
  • storage rules tied to user or tenant context
  • narrow service-role usage
  • tests that exercise cross-user boundaries

If your app can only be secure when every frontend developer remembers every ownership rule, the design is brittle.

Proving Leaks with Automated RLS Fuzzing

A lot of tools can suggest that an IDOR might exist. Fewer can prove that a real read or write leak is possible.

That distinction matters. Security teams waste time on weak signals, while developers lose confidence in the findings. If the scanner only says “you have a numeric ID in this endpoint”, the team still has to answer the hard part manually. Can another user read or modify something they shouldn’t?

Suggestion versus proof

Basic scanners are good at pattern spotting:

  • direct object IDs in routes
  • missing-looking checks in handlers
  • public functions that accept sensitive parameters
  • frontend bundles that expose internal identifiers

Those are useful leads. They are not proof.

Proof requires runtime behaviour. You need to execute requests in context, change object references, compare identities, and verify whether the application leaks data or accepts a forbidden write.

Screenshot from https://www.audityour.app/dashboard-finding-example

Why this matters for modern stacks

This is especially important in low-code, no-code, and BaaS-heavy applications. A 2025 BCS study of 300+ low-code apps found 52% vulnerable to IDOR via exposed direct object IDs in frontend bundles, contributing to 19% of data leaks in UK agencies. The same source notes a 33% rise in IDOR exploits targeting UK startups’ CI/CD pipelines, and highlights a gap around “proven leakage testing” via RLS logic fuzzing in newer no-code tooling. Those details are captured in BigID’s IDOR write-up.

That gets to the heart of the issue. Modern apps often have policy layers, generated clients, edge functions, and mobile bundles all interacting at once. The bug isn’t always obvious in code review. It appears only when a real request crosses a real trust boundary.

What RLS fuzzing should do

Good RLS fuzzing behaves like an automated reviewer who keeps asking the same question across many paths: what happens if this object belongs to someone else?

A useful system will:

  • authenticate with realistic identities
  • mutate IDs, paths, and RPC arguments
  • compare read responses across users
  • test writes, not just reads
  • verify whether table policies and functions align
  • produce evidence you can reproduce

For teams working with APIs, RPCs, and backend-integrated mobile apps, practical guidance on penetration testing APIs helps frame where this kind of runtime validation fits.

The best outcome from an IDOR test isn’t “possible issue found”. It’s “here is the exact request, identity, object, and response that proves the leak”.

That’s what closes the loop between security and engineering. Developers can reproduce it, fix it, and verify that the regression is gone.

Your IDOR Prevention Checklist

Use this as a build and review checklist, not just a security checklist. Insecure direct object references usually enter during normal feature work.

Build rules that hold up in production

  • Check object access on the server every time
    Every route, RPC, edge function, and callable function that touches a private object needs an explicit authorisation decision.

  • Treat authentication and authorisation as separate controls
    A valid session doesn’t grant access to every object reachable by ID.

  • Prefer non-guessable identifiers externally
    UUIDs and GUIDs reduce easy enumeration, but still require ownership checks.

Lock down the data layer

  • Enable and review RLS on sensitive Supabase tables
    Policies should match real ownership and tenant rules for reads and writes.

  • Keep service-role usage narrow
    Don’t run user-originated requests with increased privileges unless the function enforces strict access rules itself.

  • Audit storage paths and download handlers
    Files, transcripts, exports, and generated assets are frequent IDOR targets.

Test the paths attackers actually use

  • Replay requests with another user’s object IDs
    Test both read and write operations. Some of the worst leaks are blind writes.

  • Use at least two accounts and multiple roles
    Single-user testing misses cross-user and privilege-boundary problems.

  • Fuzz IDs, paths, and RPC inputs
    This catches the routes your manual review won’t cover consistently.

Remove common false confidence

  • Don’t rely on hidden fields or disabled UI controls
    Attackers don’t need your UI to send requests.

  • Don’t assume frontend route guards are security controls
    They improve UX. They do not enforce data access.

  • Don’t assume UUID adoption solved the issue
    It only changes how easily IDs are discovered.

Review the risky areas first

  • Supabase RPCs and edge functions
    These often sit just outside the main policy review.

  • Firebase callable functions and document fetches
    context.auth is not the same as object-level permission.

  • Mobile app bundles and frontend state
    If object IDs, keys, or resource paths are exposed, the backend must still hold the line.


If you’re building on Supabase, Firebase, or mobile backends and want to verify actual read and write leakage instead of guessing, AuditYour.App is a practical next step. It scans modern stacks for weak RLS, exposed RPCs, leaked secrets, and mobile bundle issues, then uses runtime fuzzing to prove whether cross-user access is really possible. That’s useful when you need clear evidence, reproducible findings, and remediation guidance your team can act on quickly.

Scan your app for this vulnerability

AuditYourApp automatically detects security misconfigurations in Supabase and Firebase projects. Get actionable remediation in minutes.

Run Free Scan