IDOR vulnerabilities: why scanners miss them, and why AI-written code makes them more common

Summary

An IDOR (insecure direct object reference) is when your app takes an ID from a request, looks up that record, and hands it back without checking the caller is allowed to see it. Change the invoice ID in the URL, get someone else's invoice.

It's the most common business logic flaw we see in SaaS codebases, and broken access control is still number one on the OWASP Top 10 for 2025. It's also the flaw pattern-matching scanners are worst at, because the vulnerable code looks completely normal. Coding agents write a lot of it.

What one looks like

Here's an endpoint a coding agent could write in about four seconds:

app.get('/invoices/:id', requireAuth, async (req, res) => {
  const invoice = await db.invoice.findUnique({ where: { id: req.params.id } })
  res.json(invoice)
})

‍

It checks the caller is signed in. It never checks the invoice belongs to the caller's team. The fix is one line of scoping:

const invoice = await db.invoice.findFirst({
  where: { id: req.params.id, teamId: req.user.teamId }
})

‍

Why scanners miss it

Traditional SAST matches code against patterns that look dangerous: user input in a SQL string, a call to eval, a hard-coded key. The endpoint above has auth middleware and a parameterized query. Nothing matches, so the scanner reports zero issues and everyone feels good.

The flaw only shows up if you know something the code doesn't say: invoices belong to teams, and every other invoice route in this service checks teamId. That's business context. A scanner reading one file at a time doesn't have it.

‍

routes/teams.ts - DELETE /teams/:id/members/:userId
24  requireAuth(req)
25  await removeMember(req.params.id, req.params.userId)
Pattern matching
0 issues
Auth is called, so nothing matches a known bad pattern.
With codebase contextHigh
Missing team admin check
Every other team route checks the caller is an admin. Here, any member can remove the owner.

‍

DAST doesn't fare much better. Spotting an IDOR from the outside takes two accounts, knowing which records belong to which, and a reason to try swapping IDs on that particular endpoint.

‍

Why agents write so many of them

Agents are very good at writing an endpoint that works. Their tests sign in as one user and fetch that user's own records, so everything goes green. Meanwhile your ownership rules live somewhere else entirely, in middleware on some routes, a query helper on others, and someone's head for the rest. The agent sees the file it's editing.

You might have "always scope queries by team" in your AGENTS.md. In OctoBench, the best model followed every instruction in a task only 28% of the time, and Claude Sonnet 4.5 and Gemini 3 Pro managed about 15%. And with agents opening more than 40% of GitHub pull requests, a missing filter on one line of code in a 400-line diff is easy to wave through.

The flaws also tend to stick around. Researchers looked at 302,600 AI-authored commits across 6,299 GitHub repos and found AI introduced about 1.5 times as many security issues as it fixed. Of the issues it introduced, 22.7% were still in the latest version of the code.

‍

The rest of the family

IDOR is the most common, but it has siblings that hide in normal-looking code. A role check that confirms you're on the team but not that you're an admin, so any member can remove the owner.

An update handler that copies the whole request body onto the record, so a user can set their own plan to enterprise. A refund endpoint that never checks the order was paid. A server that trusts the total the browser sent. Two requests at once that redeem the same credit twice. None of them trip a pattern. All of them show up in incident write-ups.

‍

How to find and prevent them

  1. Write down who owns what. Which records belong to a user, a team or an org, and how each route enforces it. A short threat model is plenty.
  2. Scope at the data layer. Make tenant filters part of every query by default, through a query helper or row-level security, so a route has to opt out rather than opt in.
  3. Don't count on unguessable IDs. UUIDs leak through URLs, logs and exports. Treat every ID in a request as untrusted.
  4. Test with two accounts. For every new endpoint, try reading and changing the other account's records.
  5. Review every PR with codebase context. The cheapest place to catch an IDOR is review, by a reviewer who knows every other invoice route checks teamId.

‍

How Dam Secure catches them

Add invoice detail endpoint #731
Openclaude-code wants to merge 1 commit into main from feat/invoice-detail
Conversation 2Commits 1Checks 1Files changed 1
src/routes/invoices.ts+4
14+ app.get('/invoices/:id', requireAuth, async (req, res) => {
15+ const invoice = await db.invoice.findUnique({ where: { id: req.params.id } })
dam-securebotreviewed in 1m 33s
High
Any signed-in user can read another team's invoices
The lookup uses the ID from the URL without scoping it to the caller. Every other invoice route filters by teamId, so this one is the odd one out. Reachable from the public API.
Suggested change
- findUnique({ where: { id: req.params.id } })
+ findFirst({ where: { id: req.params.id, teamId: req.user.teamId } })
Commit suggestionRanked by internet reachability
16+ res.json(invoice)
17+ })

‍

At onboarding, Dam Secure maps how your codebase fits together, including services, data flows, auth and the paths an attacker could actually take.

Every PR review and full codebase review checks changes against that map. So when every other team route checks the caller is an admin and the new one doesn't, it gets flagged on the first review, ranked by whether it's reachable from the internet.

Your threat models feed in too, so if tenant data leaks are the thing you worry about most, every change gets checked for one.

When it finds an IDOR, the fix becomes security context your coding agents follow, so the next service doesn't ship the same flaw.

‍

"Dam Secure is the equivalent of a principal security engineer reviewing every change, making sure it's ready for production." Doug Rathbone, VP of Engineering, Dovetail

‍

FAQ

Is IDOR the same as broken access control?

IDOR is one type of broken access control, which is A01 on the OWASP Top 10 for 2025. In API security the same flaw usually goes by BOLA (broken object level authorization).

Can SAST find IDOR?

Pattern-matching SAST usually can't, because nothing in the code looks wrong. AI-native SAST that understands your codebase's ownership model can.

Do UUIDs prevent IDOR?

No. They make IDs harder to guess, but IDs still leak. Only an authorization check prevents IDOR.

Want to see which business logic flaws are already in your codebase? Speak to our team.

GET STARTED WITH DAM SECURE

Your Security.
Enforced at AI Speed.

Attackers are already using Mythos-level models to find vulnerabilities. Join the teams putting frontier models on your side first.