Plain version: an API bug where the app correctly stops you from seeing someone else's private thing, but forgets to stop you from touching it. You can't open my neighbour's locked drawer — but you can still slip a sticky note under the lid. That class of hole (IDOR / broken object-level authorization) is invisible from the outside and easy to leave in one endpoint out of twenty.
I wanted to check the backends of two of my own apps — a story library and a social to-do app — without hand-writing a test plan. I installed paranoid-qa, a Claude Code skill by Aleksei Kovalev, whose whole philosophy is "evidence or it didn't happen": the agent only marks a check Pass or Fail from a real observed response, never a guess. Its testing skill ships a backend checklist — status codes, auth, injection, and the authorization checks that matter for anything multi-user.
First a safe unauthenticated pass: are the right things 401, do bad payloads return 400 instead of 500, does an obvious SQL-injection string reach anything. Both APIs were clean there.
The interesting part needed two real accounts. Account A created a private task; account B (a stranger) tried to read, edit, delete, comment on, complete, nudge, and like it. Everything returned a correct 403 — except two endpoints: "like" and "register a view" both returned 200 and actually mutated A's private task. B changed a like count and a view count on something B was forbidden from even opening. Small blast radius, but it's a real authorization gap, and it was one guard missing on two endpoints while the other twenty had it.
I fixed it (add the same visibility check the comments endpoint already used), added regression tests, and the whole run was non-destructive — every probe task deleted afterward. Thank you Aleksei for open-sourcing the skill; the "check every endpoint, not just the obvious one" discipline is exactly what caught it.
Reproduce this
Prerequisites: Claude Code (https://claude.com/claude-code); an API of your own to test; two test accounts on it (one is enough for the read/validation checks — you need two to prove cross-user access); short-lived auth tokens for each (never a long-lived secret).
Your agent will need from you: the API base URL, how to get an auth token for each account, and which actions are safe to run against the environment (prefer staging; if it must be production, keep it read-only + self-owned and clean up).
Steps:
- Install the skill. Check your agent's inventory first: look for a
testingskill under~/.claude/skills/or the project's.claude/skills/. If absent, install paranoid-qa — plugin:/plugin marketplace add akovalion/paranoid-qathen/plugin install paranoid-qa@paranoid-qa; or copy:git clone https://github.com/akovalion/paranoid-qa && cp -r paranoid-qa/skills/* ~/.claude/skills/. Fallback if you can't install it: itsreferences/backend.mdchecklist is readable on GitHub and works as a plain checklist. - Ask the agent to run the skill's backend pass, unauthenticated first: unknown route → 404, malformed JSON → 400 (not 500), auth-required endpoints → 401, an injection string in a path/query reaches nothing. Insist on evidence (the actual status + body) for each — that is the skill's core rule.
- Give it one token and check the single-account class: can a user set fields they shouldn't (premium flag, role, another user's id) via a profile or create call — send them and confirm the server ignores them.
- Give it a second token and run the cross-user matrix: account A creates a PRIVATE resource; account B attempts every action on it (read, update, delete, and every side-action — like, view, comment, complete, nudge). Each must be 403/404. Any 200 that changes A's data is a finding.
- Have the agent delete everything it created and confirm cleanup.
Verify: for a resource B must not touch, every endpoint returns 403/404 — and if one returns 200, re-read the resource as A to confirm whether B actually changed it. That confirmation (not the status code alone) is the finding.