Home / Blogs / How to prove an IDOR: the 3 requests a VAPT report skips
How to prove an IDOR: the 3 requests a VAPT report skips
Guardial Team 8 mins read
An IDOR is the vending machine of web bugs. You put in someone else's number and out comes their soda. The machine did not check whose hand was pressing the button, and your API did not check whose session was holding the id.
The bug is easy to describe and annoying to prove, which is why so many reports stop halfway. "Parameter appears user controllable, IDOR possible" is where the effort ends and the arguing begins. Here is the exact test that separates a real finding from a guess, and why it takes three requests, not one.
Why "possible IDOR" is not a finding
Open most VAPT PDFs in India and you will find that line: "parameter appears user controllable, IDOR possible." That sentence tells engineering nothing. Can they replay it? No. Does it prove that another user's data actually leaked? Also no. It is a horoscope, not a finding.
The cost of that laziness shows up later. A developer reads "possible IDOR" and reasonably asks: possible how? With which account? Against which object? The report has no answers, so the ticket bounces between security and engineering for two weeks, and the actual bug, if it is real, ships to production while everyone debates it. A finding needs a witness. The witness is three HTTP requests, run in order, with two different accounts.
The three requests that close the case
1. The baseline: user A reads their own object
Log in as user A and request the object normally, something like GET /api/invoice/1024. Capture the full response, headers included, and save it somewhere you will not lose it. This is your evidence locker.
It does two jobs. First, it proves the endpoint works and returns real data, so nobody can claim later that your test hit a broken route. Second, it gives you the fingerprint of A's data. When the second response arrives, you compare field by field. Matching names, amounts, and ids mean you really did receive A's object, not a similar-looking public record.
2. The swap: user B asks for A's object
Now log in as user B, an account with no legitimate reason to see invoice 1024. Not a role with admin rights, not the same user on a second browser, but a genuinely separate account that the business would never expect to hold that invoice. Replay the exact same request with B's token and nothing else changed. Same headers, same parameters, only the session cookie or token differs.
If the server returns A's invoice, the access check is missing or trusts the id blindly. That is the IDOR. If it returns 403 or 404, the application is doing its job and you move on. Skipping this request is the single most common reason IDOR reports get rejected, because a scanner flagging "id in URL" proves the URL has an id. Groundbreaking. It says nothing about who is allowed to see what is behind it.
3. The control: ask for an object that does not exist
With the same B session, request GET /api/invoice/999999, an id that was never issued. A healthy server answers 404. This control request matters more than people think, and almost nobody runs it.
Suppose the fake id also returns data. Now your "IDOR" might be a demo mode, a shared cache, a mock endpoint, or an environment where everything returns a canned response. Without the control, you cannot tell the difference, and your report collapses at the first engineering question. The control is the lie detector. It tells you the server's 403s and 404s mean something, so the success response in step two means something too.
The excuses you will hear, and the answers
"The id is random, so an attacker cannot guess it." IDs do not need to be guessed. They leak in logs, referer headers, screenshots, support tickets, and email links. If possession of the id equals authorization, the check does not exist.
"You need a valid session to hit this endpoint." That is the point. Every user has a valid session. The question is whether one user's session can read another user's object, and steps two and three answer it.
"It only works with test data we seeded." The control request handles this. If fake ids behave differently from real ones, you will see it in step three and you have not wasted anyone's time.
The verdict rule
An IDOR is confirmed when two things happen together: user B receives user A's object, and the fake id is rejected. Miss either condition and you have a suspicion, not a vulnerability. Both conditions, and the argument is over before it starts.
This is the same discipline Guardial applies on every run. It keeps two sessions alive, replays the object across them, fires the control request, and attaches all three exchanges to the finding. Not a screenshot of a scanner flag, the actual traffic.
The report then reads like proof instead of a prediction. Engineering gets the request, the two sessions, and a result they can replay in thirty seconds with curl. Nobody has to take the scanner's word for it, and nobody has to schedule a meeting to figure out whether the bug is real. The three requests already settled that.