GuideBy Yusuf, VXSecPublished November 7, 2026 ยท Updated November 7, 2026

How to test your app for IDOR, and fix it

IDOR is when changing an ID in a link shows you someone else's data. Here's how to test your app for it and fix it: let your AI coding tool do it, or do it yourself in five minutes.

Only test apps you own or have written permission to test.

What is IDOR?

Your app shows an invoice at /invoices/1042. It checks that you're logged in, but not that invoice 1042 is yours. Change it to 1041 and you're reading someone else's.

IDOR stands for insecure direct object reference. OWASP calls it broken object level authorization (BOLA) and ranks it first on its API Security Top 10. It's common because the app works perfectly for the person building it. The hole only shows when a second user asks for the first user's records.

It has happened at scale

  • First American Financial, 2019. Anyone with a link to one title-insurance document could change a single digit in the URL and open someone else's, with no login. About 885 million files dating back to 2003 were exposed, including bank account numbers and Social Security numbers (KrebsOnSecurity). The SEC later fined the company $487,616 over how it handled the disclosure (SEC).
  • USPS, 2018. An API behind the Postal Service's Informed Visibility program let any logged-in usps.com user look up account details of other users, about 60 million people, and even request changes to their accounts (KrebsOnSecurity).

Neither needed hacking skill: a normal link or account, and a server that never checked who was asking.

1. Let your AI do it

Open an AI coding tool that can run commands (for example Claude Code, Cursor or Codex) inside your project, and paste this:

Read https://github.com/vxsec/idor-check and follow its AGENTS.md to check this app for IDOR. Show me proof for anything you find, and fix it after I say yes.

idor-check is free and open source. It has your AI read your code, find every place that takes an ID, and prove each hole with two test accounts. You get the exact request that got in. It doesn't touch your code until you say yes, and after fixing it runs the same tests again to show each hole is closed.

2. Do it yourself

  1. Make two accounts, A and B. Use two browsers, or a normal and a private window.
  2. As B, create something private (an order, a note, an upload) and copy its link.
  3. As A, open B's link. If you see B's data, you've found IDOR.
  4. No ID in the link? Press F12, open Network, and find a request with an ID in it. As A, right-click it, Copy as fetch, paste it into Console, swap in B's ID and press Enter.
  5. Try edits and deletes the same way, then log out and try again.
Pass: "not found" or "not allowed" every time. Fail: you see, change or delete B's data even once.

Where does IDOR hide?

The page you looked at is rarely the only door. Run the same A-and-B test on each of these:

  • Downloads and exports: PDF invoices, CSV exports, receipts.
  • File storage links: uploads served from a bucket by file name or ID.
  • Invites and share links: can A accept an invite meant for B, or reuse an expired link?
  • Account settings: email change, password reset and profile updates that take a user ID.
  • Lists and search: endpoints with filters like ?user_id= or ?org_id=.
  • Create requests: sending someone else's user_id or org_id in the body (mass assignment).
  • Admin routes: hidden in the menu but still answering a normal user.
  • GraphQL and realtime: object IDs in queries, subscriptions and websocket channels.

How do I check Supabase RLS?

The public anon key in your page is normal. Row Level Security is what keeps users out of each other's rows. Run this in the SQL editor:

select tablename, rowsecurity from pg_tables
where schemaname = 'public' order by rowsecurity, tablename;

select tablename, policyname, permissive, roles, cmd, qual, with_check from pg_policies
where schemaname = 'public' order by tablename, cmd;
  • rowsecurity = false: with Supabase's default grants, anyone who opens your site can read that table.
  • qual is true on a permissive policy for anon or authenticated on a private table: permissive policies combine with OR, so this one opens it.
  • Update policies only decide which rows a user can change, not which columns. If users can update their own row, check they can't change its owner, their role or their plan.

A service_role key in your page skips RLS completely. Rotate it and keep it on the server.

How do I fix IDOR?

The server takes the user from the verified session and only loads records that belong to that user. Put the owner in the query itself, and treat "not yours" the same as "doesn't exist". In an Express-style route:

// Before: any logged-in user gets any invoice
const id = Number(req.params.id); // numeric IDs; skip for UUIDs
const invoice = await db.invoice.findFirst({ where: { id } });
res.json(invoice);

// After: only the owner's invoice, otherwise 404
const invoice = await db.invoice.findFirst({
  where: { id, ownerId: req.user.id }, // req.user from the session
});
if (!invoice) return res.status(404).end();
res.json(invoice);

On Supabase, the same rule lives in the database. Tie each policy to auth.uid(), and use with check so a user can't hand a row to someone else:

alter table invoices enable row level security;

create policy "owner reads" on invoices for select to authenticated
  using ((select auth.uid()) = owner_id);

create policy "owner creates" on invoices for insert to authenticated
  with check ((select auth.uid()) = owner_id);

create policy "owner updates" on invoices for update to authenticated
  using ((select auth.uid()) = owner_id)
  with check ((select auth.uid()) = owner_id);

-- Columns users must never change (role, plan): grant only the safe ones
revoke update on profiles from authenticated;
grant update (full_name, phone) on profiles to authenticated;

Check edits, deletes and downloads, not just reads. If you found a hole by hand, paste this into your AI tool:

Go through every route, API endpoint, server action and database query in this app that takes an ID from the user. Make sure the server checks that the logged-in user is allowed to read, update or delete that record. Never trust an ID or user id sent from the browser. If this app uses Supabase: turn on Row Level Security for every table, drop policies that allow all rows (like using (true)) unless that table is meant to be public, and give each private table only the policies the app needs (select, insert, update or delete), each tied to auth.uid(). Don't add permissions the app doesn't use. Keep anything that's meant to be public or shared working. Then write a test where user A tries to read, edit and delete user B's records, and to create a record with B's user id, and make it pass.

Then test again yourself. Don't take the AI's word for it.

Questions people ask

Are UUIDs or random IDs enough to stop IDOR?

No. Random IDs make guessing harder, but they leak in links, emails, exports, logs and browser history, and anyone who gets one can use it. The server still has to check that the person asking may see that record.

What is the difference between IDOR and BOLA?

They are the same bug. IDOR (insecure direct object reference) is the older OWASP name; BOLA (broken object level authorization) is the name in the OWASP API Security Top 10, where it ranks first. Both fall under broken access control.

Does Supabase Row Level Security stop IDOR?

For requests that go through the Supabase client, yes, if every table has RLS on and the policies tie rows to auth.uid(). It does nothing for your own server routes that use the service_role key, which skips RLS, so those routes need their own ownership check.

Can a scanner find IDOR automatically?

Not reliably. A generic scanner doesn't know which record belongs to which user, so it can't tell a leak from a normal response. Someone has to know who should see what, which is why the method here signs in as two users and compares.

Is it legal to test for IDOR?

On your own app, or one you have written permission to test, yes. Opening someone else's records on a site you don't own can break computer misuse laws, even if all you changed was a number in the link.

How do I prevent IDOR in new code?

Load every record through a query that includes the current user, deny by default, and never take the user or role from the request. Add a test where user A tries user B's records, and run it on every change.

What this doesn't cover

Team-to-team access and multi-step flows need the same idea applied by hand: one user's session, another user's ID. A clean result means these checks passed, not that the app is secure.

Further reading

Want to practise on an app built to be broken? Try the free Breach Lab. Want someone to test your app for you? See Hack My App. Need the fix done? See VXSec services.