0x55aa
← Back to Blog

IDOR: The Vulnerability Hiding in Your API (And Why It's Embarrassingly Easy to Miss) 🎯

β€’7 min read

IDOR: The Vulnerability Hiding in Your API 🎯

Picture this: you're logging into an e-commerce app and you notice the URL says /orders/1042. You type /orders/1041. Boom β€” you're looking at someone else's order, their name, their address, their purchase history.

Congratulations, you just found an IDOR.

And the scary part? In my experience building production systems, IDOR bugs are shockingly common β€” even in codebases that have everything else locked down tight. Auth middleware? Check. Rate limiting? Check. SQL injection protection? Check. Checked whether this particular user should see this particular order? ...crickets. πŸ¦—

What Even Is IDOR? πŸ€”

Insecure Direct Object Reference (IDOR) is when your app lets users access objects β€” database records, files, anything β€” by guessing or incrementing an identifier, without verifying they're allowed to see it.

The name sounds fancy. The concept is painfully simple:

You're only checking if the user is logged in. You forgot to check if they own the thing they're asking for.

It's like a hotel that checks if you have a keycard… but opens any room you swipe it on. "Yes sir, you're a guest. Room 1204? Right this way. Oh, that's not YOUR room? We didn't check for that."

The "It Can't Be This Simple" Bug 😬

In security communities, we often discuss how the flashiest vulnerabilities get all the attention β€” memory corruption, zero-days, RCE chains. But IDOR is the unglamorous workhorse that shows up in every single bug bounty program.

Here's the classic vulnerable pattern:

// Node.js / Express β€” DANGEROUS ❌
app.get('/api/invoices/:id', authenticate, async (req, res) => {
  const invoice = await Invoice.findById(req.params.id);
  res.json(invoice);
});

That authenticate middleware just checks if you're logged in. It says nothing about whether invoice #5738 belongs to you.

An attacker loops from 1 to 99999 and downloads your entire customer invoice database before lunch. Nice.

The fix is literally one line:

// Node.js / Express β€” SAFE βœ…
app.get('/api/invoices/:id', authenticate, async (req, res) => {
  const invoice = await Invoice.findOne({
    _id: req.params.id,
    userId: req.user.id  // πŸ‘ˆ this one line saves you
  });

  if (!invoice) return res.status(404).json({ error: 'Not found' });
  res.json(invoice);
});

Now the query only returns the invoice if it also belongs to the authenticated user. Someone trying to access invoice #5738 that isn't theirs? Gets a 404. They don't even know the invoice exists.

Laravel Does This Better (But You Still Gotta Use It) πŸ”΄

In my day job building Laravel backends, we use policies and scoped queries. Laravel gives you all the tools β€” but tools don't swing themselves.

// Laravel β€” DANGEROUS ❌
public function show(Invoice $invoice)
{
    return response()->json($invoice); // No ownership check!
}

// Laravel β€” SAFE βœ…
public function show(Invoice $invoice)
{
    $this->authorize('view', $invoice); // Throws 403 if not owner
    return response()->json($invoice);
}

Or even better, scope your queries at the relationship level so you never accidentally fetch records you shouldn't:

// Scope all queries to the current user's data
$invoice = auth()->user()->invoices()->findOrFail($id);
// If $id belongs to someone else β†’ 404. Clean and safe.

Pro Tip: If you're using Model::find($id) anywhere in a controller without a user scope, that's your IDOR waiting to happen. grep it. Right now. I'll wait.

Real Talk: How I Found My First IDOR πŸ’¬

As someone passionate about security, I started doing bug bounty hunting on the side. My very first submission was an IDOR β€” embarrassingly simple, but the company paid out anyway.

The target was a SaaS invoicing tool. After signing up, I went to my invoice list and clicked one. URL: /invoices/247. I changed it to /invoices/246. Got someone else's invoice, business name, tax ID, everything.

I reported it. They said "working as intended" (πŸ™„). I escalated with a proof-of-concept showing I could enumerate all ~12,000 invoices in under 3 minutes with a simple script. They responded faster the second time.

The fix they shipped? Literally the one-liner I described above. The dev had copy-pasted a resource controller and never added the ownership check.

This happens. All the time. Even to good developers.

UUIDs Help, But Don't Save You 🎲

A common response to IDOR is "we use UUIDs, so you can't guess the IDs!" And yes β€” switching from auto-increment integers (/orders/1042) to UUIDs (/orders/f47ac10b-58cc-4372-a567-0e02b2c3d479) makes brute force enumeration impractical.

But here's the thing: security through obscurity isn't authorization.

If a UUID leaks (in a shared link, a log file, an email), the "unguessable" argument collapses. And UUIDs are leaked way more often than people think β€” they end up in browser history, referrer headers, error messages.

UUIDs raise the bar. They don't replace proper ownership checks.

Use both. UUIDs + authorization checks. Defense in depth.

IDOR Beyond IDs: The Other Forms πŸ•΅οΈ

IDOR isn't just about numeric IDs in URLs. In security communities, we often discuss how it shows up in sneakier shapes:

File downloads:

GET /download?file=report_user_99.pdf
β†’ try: /download?file=report_user_1.pdf

Account settings via POST body:

POST /api/settings
{ "userId": 99, "email": "[email protected]" }
β†’ change userId to 1 and update the admin's email

API filters that expose other users' data:

GET /api/messages?userId=99
β†’ change to: /api/messages?userId=1

Any place you're passing a reference to a database object? That's a potential IDOR. The fix is always the same: verify the relationship between the authenticated user and the requested object, server-side, every time.

Your IDOR Audit Checklist πŸ“‹

Before you deploy that API, grep through your codebase for these patterns:

  • Any findById / find($id) without a user scope?
  • Any file download endpoints using user-supplied filenames?
  • Any POST/PUT endpoints that accept a userId in the body?
  • Any admin-only resources returned without role checks?
  • Checked your authorization policies cover ALL HTTP methods (not just GET)?

If you're in Laravel: run php artisan route:list and look at every route that takes an {id} parameter. Then ask: "does the controller check ownership, or just existence?"

The Hierarchy of IDOR Severity πŸ”₯

Not all IDORs are equal. Bug bounty programs rate them like this:

Impact Severity Example
Read your own data type Low View other user's profile picture
PII exposure Medium Read another user's address/email
Financial data High Access other users' invoices/payments
Write/modify access Critical Change another user's password or settings
Admin escalation Critical Promote yourself to admin

Even "low severity" ones are worth fixing β€” they signal systemic authorization problems.

TL;DR 🏁

IDOR is the vulnerability that says: "yes you're authenticated, but are you authorized?" β€” and it's one of the most common findings in real-world applications because it's invisible to functional testing. Your login tests pass. Your unit tests pass. But nobody thought to test "what happens when user A requests user B's resource?"

Authentication β‰  Authorization. Tattoo it on your keyboard.

Add that one extra query condition. Scope your database queries to the authenticated user. Use framework policies. And audit your existing routes β€” you might be surprised what you find.

As someone from security communities like YAS and InitCrew, I can tell you: this is the bug that keeps paying out on bug bounty programs because developers keep forgetting it. Don't be that developer.


Spotted an IDOR in the wild? Got questions? Find me on LinkedIn or GitHub. Let's talk security!

Go audit your routes. I'll wait. πŸ”’