Teardown: how an IDOR quietly exposes another user’s data

Teardown: how an IDOR quietly exposes another user's data

This is an idor example built from scratch so you can watch how one quietly exposes another user’s data. We will use an invented app called Acme Notes, map how it works, form an assumption about a weak spot, then test it with real requests. Nothing here touches a live system. The goal is to teach how the bug works and how to spot it before an attacker does.

What an idor example actually is

IDOR stands for insecure direct object reference. It happens when an app uses an id from the request to look up a record, but never checks that the person asking is allowed to see that record. The id is the direct object reference. When ownership is not verified, the reference becomes insecure. That gap is the whole bug.

This bug is common for one reason. Developers think about authentication, who you are, far more than authorization, what you are allowed to touch. Acme Notes asks you to log in. It forgets to ask whether the note you requested is yours.

An IDOR is rarely about a clever payload. It is the server trusting a number it should have checked.

Step one: map the app like a researcher

Before testing anything, understand how the app is meant to work. Acme Notes is a small notes tool. You sign in, you see a list of your notes, you click one to read it. Open the browser network tab and watch what the page sends. When you click a note, the front end makes this request:

GET /api/notes/4012 HTTP/1.1
Host: app.acmenotes.example
Authorization: Bearer eyJhbGciOiJI (your token)
Accept: application/json

The server answers with the note as JSON:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 4012,
  "owner_id": 88,
  "title": "Q3 launch checklist",
  "body": "Ship the billing page before Friday."
}

Two facts stand out. The note id, 4012, is a plain sequential number that sits right in the URL. The response also carries an owner_id. Your account is owner 88. So the app knows who owns the note. The question is whether it checks that ownership on every read.

Step two: form the assumption

Good testing starts with a guess you can prove or disprove. Here the assumption is direct: the server may load a note by id without confirming the requester owns it. Sequential ids make this worth testing, because note 4011 and note 4013 almost certainly belong to other users. If the server only checks your token and then trusts the id, you can read notes that are not yours.

An attacker would form the same assumption. The difference is that a researcher tests it on an app they control or have permission to test, and reports it so it gets fixed.

Step three: test by requesting a neighbouring id

Keep your own valid login. Change only the id in the URL. Ask for the note next door:

GET /api/notes/4011 HTTP/1.1
Host: app.acmenotes.example
Authorization: Bearer eyJhbGciOiJI (your token)
Accept: application/json

If the app is safe, you should get a refusal. Something like this:

HTTP/1.1 403 Forbidden
Content-Type: application/json

{ "error": "You do not have access to this note." }

But Acme Notes is not safe. It returns the note in full:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 4011,
  "owner_id": 73,
  "title": "Investor call notes",
  "body": "Runway is tight. Do not share outside the board."
}

Look at owner_id. It is 73, not 88. You are logged in as 88, yet the server handed you another user’s note. That is the bug, proven in one request.

Step four: confirm it is real, not a guess

One odd response is not proof. Before you call this a finding, rule out the boring explanations. A careful check answers a few questions.

  • Is the data really someone else’s? The owner_id in the response differs from your account id. Log in as a second test user, note their real id, and confirm the leaked note belongs to a third party, not to you under another label.
  • Does it repeat? Request 4010, 4009, 4008. If a range of ids you do not own all return 200 with full bodies, this is a pattern, not a fluke.
  • Is the token doing anything? Send the same request with no Authorization header. If that returns 401 but a valid token for the wrong user returns 200, the app checks login but not ownership. That is the exact shape of an IDOR.
  • Can you see the write side too? Try a read only method first. Only test edits or deletes on data you are allowed to change, so you never damage real records while confirming the issue.

When the leaked owner id is consistently not yours, the behaviour repeats across a range, and a valid login is the only thing the server checks, you have evidence rather than a hunch. That is the line between a real finding and noise. For more on how access control bugs are grouped and tested, see access control.

Step five: assess the impact

Impact is about what an attacker can reach and how easily. In Acme Notes, ids are sequential and the endpoint returns full note bodies. A script can count from 1 upward and pull every note in the system in minutes. That turns one weak check into a full data exposure.

Now widen the lens. The same pattern often appears on more than one route. If /api/notes/{id} is broken, test the siblings the same way:

  • /api/invoices/{id} for billing records
  • /api/users/{id}/profile for personal details
  • /api/files/{id}/download for attachments

One missing ownership check is bad. The same check missing across several endpoints is how a small bug becomes a breach. This is why one confirmed finding is worth turning into a repeatable test, so the same gap cannot return on a new route later.

How to fix an insecure direct object reference example

The fix is not to hide the id or scramble it. Hiding the reference only slows an attacker down. The real fix is to check ownership on the server, on every request, every time.

Check ownership at the data layer

Bind the lookup to the logged in user. Instead of fetching a note by id alone, fetch it by id and owner together:

-- weak: trusts the id from the request
SELECT * FROM notes WHERE id = 4011;

-- safe: ties the note to the caller
SELECT * FROM notes
WHERE id = 4011 AND owner_id = :current_user_id;

If the second query returns no rows, the app returns a 404 or 403. The user never learns whether the note exists, so they cannot map your id space by probing.

Centralise the rule and add a regression test

Put the ownership check in one place that every route calls, not copied into each handler where one can be forgotten. Then write a test that logs in as user A, requests user B’s note, and fails the build if the response is anything but a refusal. That test is what keeps the bug from coming back during the next refactor.

What to take away

An IDOR is a trust mistake, not a complex exploit. The app trusts an id it should have checked against the logged in user. You find it by mapping the app, noticing a guessable reference like a sequential note id, assuming ownership might not be verified, and proving it with a single request that returns someone else’s data. You fix it by checking ownership on the server for every object, every time.

This is exactly the kind of bug an autonomous researcher that tests an app’s assumptions is built to find, because it comes from understanding how the app should behave, not from matching a known payload. If that approach is useful to you, read more about UnboundCompute.