Peeking at someone else’s account
Watch it happen
Mallory is logged in as account 1042 and asks the teller for account 1043. The teller hands over Alice’s statement without checking whose it is.
Mallory is a real customer. At the door she proves who she is and gets in. So far, everything is fine.
Mallory says: Hi, I’m Mallory.In bank terms and app terms
| In the bank | In your app |
|---|---|
Are you exposed?
Your API loads a record by ID straight from the URL or request body. You only test by logging in as yourself and clicking around. IDs are guessable numbers (1, 2, 3…) rather than random.
On every request, check the record belongs to the current user before returning it. Put the ownership check in the query itself: WHERE id = ? AND user_id = ?. Use random IDs as a second layer, never as the only layer.
It has happened to real companies
First American Financial (2019) — A flaw on First American's website exposed about 885 million files, including bank account numbers and statements, mortgage and tax records and Social Security numbers. No authentication was required: changing one digit in a document link returned other people's records, which suggested the document numbers were sequential. The company was alerted by a journalist in May 2019, and in 2021 the SEC fined it $487,616 because senior executives had not been told that its security team had found the flaw earlier.
Parler (2021) — In January 2021, as AWS was about to cut Parler off, an archivist pulled the URLs of every public post "in sequential order, from the very first to the very last" and archived them, and Parler had not stripped metadata such as GPS coordinates from uploaded media. Sequential, guessable IDs make bulk downloads like this easy.
What it looks like in code
// BAD: No ownership check — any authenticated user can access any task
export async function GET(request, { params }) {
const task = await db.getById("tasks", params.id)
return NextResponse.json(task)
}
// BAD: User ID comes from request body, not from auth token
export async function GET(request) {
const { userId } = await request.json()
const profile = await db.getById("profiles", userId)
return NextResponse.json(profile)
}
Ask your AI tool to fix it
Paste this into Cursor, Claude, Lovable, Bolt or whatever you build with.
Review every API route that reads, updates or deletes a record by ID. For each one, make sure the query also filters by the authenticated user’s ID (or checks ownership) and returns 404 or 403 when it does not match. List the routes you changed.Where this comes from
The facts, figures and fixes on this page were checked against these sources on 25 Oct 2026. They are linked so you can check for yourself.
- API1:2023 Broken Object Level AuthorizationOWASPCheck ownership on every request that uses a client ID; random IDs are a preference, not the fix
- Insecure Direct Object Reference Prevention Cheat SheetOWASPVerify permission on every access; random IDs alone are not enough
- How to think about data security in Next.jsNext.jsAuthorization means checking the user owns this specific record, not just being logged in
- First American Financial Corp. Leaked Hundreds of Millions of Title Insurance RecordsKrebsOnSecurity885 million files, no login, one digit changed in a link, IDs likely sequential
- SEC Charges Issuer With Cybersecurity Disclosure Controls FailuresU.S. SECFirst American fined $487,616; executives were not told of the earlier finding
- The Hacker Who Archived Parler Explains How She Did It (and What Comes Next)VICEParler posts pulled in sequential order; uploaded media kept GPS metadata
Spot a mistake or something out of date? Report a correction · How these guides are made and checked
The free, open-source scanner checks for most of the problems in these guides.