DEV Community

mohammed faizan mohiuddin
mohammed faizan mohiuddin

Posted on

I made my AI agent break into its own code — and it smashed an IDOR

Summer Bug Smash: Smash Stories 🐛🛹

 This is a submission for DEV's Summer Bug Smash: Smash Stories powered by Sentry.

The scariest bugs aren't the ones that crash. They're the ones that work perfectly in every demo and quietly hand your data to the wrong person.

Here's one that ships constantly — I've watched AI coding agents write it, confidently, and end the turn with "Done ✅." So I built something to catch it, and this is the first bug it smashed.

The setup

Ask any coding agent for the most boring endpoint in the world:

"Add an endpoint to fetch an invoice by id."

You get this back, and it looks fine:

app.get('/invoice/:id', async (req, res) => {
  const invoice = await db.query(
    `SELECT * FROM invoices WHERE id = ${req.params.id}`);
  res.json(invoice);
});
Enter fullscreen mode Exit fullscreen mode

It passes the demo. GET /invoice/1 returns invoice #1. Ship it.

The smash

There are two bugs hiding in those four lines, and both are invisible on the happy path:

  1. IDOR (broken object-level authorization). Nothing checks that the invoice belongs to the caller. GET /invoice/2 as user 1 cheerfully returns another tenant's invoice. This is the #1 item on the OWASP API Security Top 10 — and it's a one-liner to introduce.
  2. SQL injection. req.params.id is interpolated straight into the query string. GET /invoice/1;DROP TABLE invoices-- is now a conversation you're having with your database.

Neither shows up when you test with your own account. That's exactly why they ship.

What caught it

I'd gotten tired of "Done ✅" on code like this, so I built a skill for my AI agent called the Attacker. The rule is simple: after it writes anything that crosses a trust boundary — untrusted input, auth, a database query — it stops being the author and becomes the attacker. It doesn't recite an OWASP checklist; it actually tries to break this code.

Pointed at the endpoint above, it reported:

Attacked:
- hit /invoice/2 as user 1 → leaked another tenant's row (IDOR) → added owner scope
- non-numeric id → 500 with a stack trace → now a clean 400
- SQL was string-interpolated → parameterized it
Enter fullscreen mode Exit fullscreen mode

Then it fixed the bug at the boundary, not on the one path I happened to test:

app.get('/invoice/:id', async (req, res) => {
  const id = Number(req.params.id);
  if (!Number.isInteger(id)) return res.status(400).json({ error: 'bad id' });

  const invoice = await db.query(
    'SELECT * FROM invoices WHERE id = $1 AND owner_id = $2',
    [id, req.user.id],                 // ownership scope kills the IDOR
  );

  if (!invoice) return res.status(404).end();   // same 404 for "missing" and
  res.json(invoice);                            // "not yours" — don't confirm it exists
});
Enter fullscreen mode Exit fullscreen mode

Three fixes, each aimed at the class of problem:

  • Parameterized query → the injection is gone, not escaped-and-hoped.
  • AND owner_id = $2 → the row is scoped to the caller, so the IDOR can't happen even if a new route forgets to check.
  • Identical 404 for "missing" vs "not yours" → a subtle one: returning 403 for existing-but-forbidden ids leaks which invoices exist. Same response either way closes that enumeration side channel.
  • 400 on a non-numeric id instead of a 500 that dumps a stack trace.

The lesson (worth more than the fix)

The fix is boring. The reflex is the point:

"It works" in a demo is precisely the moment these bugs ship. The only code you trust is the code you already tried to break.

IDOR and injection don't survive because they're hard to fix — they survive because nobody attacks the happy path. Whether it's you, a teammate, or an AI agent writing the endpoint, the discipline is the same: the moment code touches untrusted input or a query, put on the black hat before someone else does.

I ended up bundling the Attacker into an open-source set of "senior-dev instinct" skills for AI agents called Bullpen — but you don't need any of that to steal the habit. Next time your agent says "Done ✅" on an endpoint, ask it one question:

"Now attack it."

Top comments (1)

Collapse
 
faizanmohiuddin482 profile image
mohammed faizan mohiuddin

What's a bug that only shows up when you stop testing the happy path? Drop it in the comments — I want to hear the ones that got past you.