HomeBlog › IDOR testing

IDOR: can your customers see each other's data?

Insecure Direct Object Reference is the least glamorous vulnerability class and one of the most commonly exploited. It needs no tooling to find, which cuts both ways.

IDOR illustration: two customer accounts on opposite cliff tops, each holding its own profiles, orders, invoices and support tickets, with a request to another customer's record ID crossing between them and flagged as unauthorised access.

Here is the whole vulnerability, in one exchange.

You are logged in. You open one of your own invoices and the address bar reads /invoices/1043. You change it to /invoices/1042. The page loads. It is not your invoice.

That is Insecure Direct Object Reference. There is no clever payload, no injection, no race condition. The application took an identifier from the request, looked it up, and returned it, without ever asking whether the person making the request was entitled to that particular record.

Why scanners do not find this

Automated tooling struggles here, and it is worth understanding why, because it explains the gap between "we ran a scan and it was clean" and "we are actually fine."

A scanner sees a request to /invoices/1042 return HTTP 200 with a well-formed page. That is what a correct response looks like. The scanner has no way to know that invoice 1042 belongs to a different company, because it has no model of who owns what. There is no error to detect, no anomalous status code, no malformed output. Everything about the response is normal except the one thing that matters.

Finding IDOR requires two things a scanner does not have: authenticated access to more than one account, and knowledge of which records belong to which. That makes it a manual check. It also makes it a cheap one.

The test

You need two accounts in different tenants. If your product has an organisation or workspace concept, they must be in different ones; two users in the same workspace are usually supposed to see each other's data, which tells you nothing.

  1. Sign in as account A. Create or find a record that clearly belongs to A. An invoice, an uploaded file, a project, a customer note. Something with recognisable content.
  2. Note its identifier. It will be in the URL, or in the network tab of your browser's developer tools if the app fetches it over an API.
  3. Sign in as account B, in a different browser profile or a private window so the sessions do not collide.
  4. Request A's record as B. Paste the URL. Or, if it is an API call, replay the request with B's session and A's ID.
  5. Read what comes back.

Three possible outcomes, and only one of them is good:

  • 403 or 404. The application checked, and refused. This is what you want.
  • A's data renders. You have found an IDOR. Stop and fix it.
  • A partial render, or an error page that still leaks something. A 500 that includes a record title, or a page shell that shows the customer name before failing to load the body. Still a leak, and easy to miss if you only check the status code.

Where to test, in priority order

You will not test every endpoint. Start with the ones where a leak would be worst, which is usually not the same as the ones that are easiest to reach.

  1. File downloads. Uploaded documents are frequently served from a URL that is checked less carefully than the page linking to it. A signed URL that never expires is the same problem wearing a hat.
  2. Exports and reports. CSV and PDF generation often runs through a different code path that was written later and reviewed less.
  3. Anything with an ID in the request body rather than the URL. These get tested less because they are less visible, and they are just as exploitable.
  4. Update and delete, not only read. An application that correctly refuses to show you someone else's record may still accept your request to modify it. Test the write path separately.
  5. Nested resources. /orgs/12/projects/88 may check that you belong to org 12 and never check that project 88 belongs to it.

That last pattern is worth dwelling on, because it passes a casual review. The check exists. It is just checking the wrong thing.

UUIDs are not the fix

A common response is to replace sequential integers with UUIDs so the IDs cannot be guessed. This is worth doing and does not solve the problem.

Unguessable identifiers prevent someone idly typing an adjacent number. They do nothing once an ID becomes known, and IDs become known constantly: in shared links, in support tickets, in exported spreadsheets, in URLs pasted into email, in browser history on a shared machine, in referrer headers sent to third-party analytics. An identifier is not a secret. It travels, because travelling is what identifiers are for.

The fix is an authorisation check at the point of lookup: does the authenticated principal have a right to this specific object? UUIDs raise the effort of discovery. They do not answer that question.

Fixing it properly

The durable fix is to make the safe path the default, rather than relying on each developer to remember the check.

Scope queries at the data layer so the tenant filter is structural rather than optional. Instead of fetching by ID and then checking ownership, fetch by ID and owner in the same query. A lookup that cannot express "find this record regardless of who owns it" cannot accidentally do it.

If you are on Postgres, Row-Level Security gives you this at the database level, which is the strongest version: the filter applies even to code that forgot about it. The two controls are the same idea at different layers, and RLS is the one that survives a developer's mistake.

Then add a test. A single automated test that signs in as B, requests A's record, and asserts a 403 will catch the regression when somebody adds a new endpoint next year. This is one of the rare cases where one small test covers a whole vulnerability class for a particular resource.

If you find one

Fix the endpoint first. Then ask two follow-up questions, because a single IDOR is rarely alone:

  • Was it exploited? Your access logs hold the answer, if you log the authenticated user alongside the requested resource. If you do not log that pairing, you cannot tell, and that is worth changing regardless of what you find here.
  • Where else does this pattern appear? The flaw usually reflects a habit rather than a one-off. Search the codebase for the same lookup shape.

Whether an exploited IDOR triggers a breach notification obligation depends on what was exposed and which regime applies to you. That is a question for whoever advises you on data protection, not one to settle from a blog post.

What a Trufend scan can and cannot tell you here. Trufend performs no authenticated testing. It reads what your domain exposes to an anonymous visitor: TLS, DNS, headers, email authentication, public subdomains and exposed assets. Finding IDOR requires signing in as two different customers, which no external scanner should be doing to your production system. Our report is not a SOC 2 assessment and does not cover this.

Why this one is worth your afternoon

Most of the readiness guide describes work that takes days. This one takes an afternoon at most, needs no tooling you do not already have, and tests the control that separates your customers from each other.

It is also the check that most directly answers the question a prospect is really asking when they send you a security questionnaire. Not "do you have a policy," but "can your other customers see my data." You can answer that with confidence only if you have tried it.

Questions people ask about this

What does IDOR stand for?

Insecure Direct Object Reference. It describes a flaw where an application uses a value supplied by the user, typically a record ID in a URL or request body, to look up data without checking that the requester is entitled to that record.

Why do automated scanners miss IDOR?

A scanner cannot tell the difference between a record you are allowed to see and one you are not. Both return a valid page with a 200 response. Detecting the flaw requires knowing who owns what, which means testing with two accounts and comparing.

Are UUIDs enough to prevent IDOR?

No. Unguessable identifiers make the flaw harder to stumble into, which is worth something, but they do not fix it. IDs leak through shared links, exports, emails, referrer headers and support tickets. An authorisation check is the fix; an unguessable ID is a delay.

How long does an IDOR test take?

For one endpoint, a few minutes. Two accounts, one ID swapped, one response read. Covering every endpoint that accepts an identifier takes longer, but the first test on your most sensitive object usually tells you what you need to know.

Can Trufend find IDOR on my site?

No. Finding it requires authenticated access to two accounts and knowledge of which records belong to whom. Trufend performs no authenticated testing of any kind. This is a check you run yourself, and the readiness guide describes how.

See what your domain exposes publicly

Trufend checks the outside of your domain in about thirty seconds: TLS, headers, DNS, email authentication and exposed assets.

Run a free check