Attack Patterns · 1 of 5

Broken Object-Level Authorization: How Changing One Number in a URL Is Enough to Steal Data

Published September 1, 2026 · secureFlows

This is one of the most common bugs in apps built fast, and one of the easiest to understand. This is the first post in a series on the most common attack patterns; we'll start with the simplest one, which is unfortunately also the most common.

What Is "Broken Object-Level Authorization", Really?

Every piece of data in an app usually has an "ID": an order number, an invoice number, a profile number. A properly built app checks two things before showing data: (1) is the user logged in at all, and (2) does this specific item actually belong to this user. Broken object-level authorization, or BOLA for short (you'll sometimes hear the older name, IDOR), happens when the second check simply doesn't exist. The app checks "who you are," but forgets to check "is this yours."

Think of it like an apartment building with a keypad-locked front door. You have the code, so you can get into the building. That's "logging in." But once you're inside, every mailbox in the building is unlocked to anyone who knows its number. That's exactly the difference between who you are and what you're allowed to see.

How It Actually Happens

Say you have an invoicing app, and every invoice has a URL with a number: myapp.com/invoice/1001. Dani logs in and sees his invoice at that address. Now Dani, out of curiosity or because he's an attacker, just changes the number in the address to myapp.com/invoice/1002 and hits Enter.

Diagram: the broken object-level authorization bug Dani sends two requests to the same server: one for his own invoice and one for Yael's. The server only checks whether Dani is logged in, and returns "200 OK" to both requests, including Yael's invoice, which isn't his. D Dani (logged in) D Dani (same session) GET /invoice/1001 GET /invoice/1002 (changed number) Server Only checks: Logged in? ✓ (That's it. No other check) 200 OK 200 OK Invoice 1001 Dani's: fine Invoice 1002 Yael's: leaked!
Same request, two different IDs. The server only checked "is Dani logged in", not "does this invoice belong to Dani", so it returned Yael's data too.

If the app only checked "is Dani logged in?" and never checked "does invoice 1002 belong to Dani?", the result is that Dani now sees Yael's full invoice, including the amount, her business details, and maybe payment info too. The scary part: this requires no special technical knowledge at all. It's the browser's address bar turned into a hacking tool.

An Example from the Press

In July 2025, TechCrunch reported that Meta fixed a bug of exactly this kind in its Meta AI chatbot feature. Every conversation with the AI had an ID that could be changed in the request, and whoever changed the ID got back a private conversation belonging to a completely different user, including the questions they asked the AI and the answers they received. Meta fixed the bug after it was responsibly disclosed, and no evidence of real-world abuse was found, but the bug existed in a feature launched to the general public, at a company with one of the largest security teams in the world. If it happened there, it can happen in any app.

Read the article on TechCrunch →

How to Fix It

The only fix that actually works is server-side ownership checks, on every request that returns or modifies a specific item: "does this item actually belong to the logged-in user?" It's not enough to check that the user is logged in. It's not enough to check that the ID is valid. You need to explicitly check that ownership matches on every endpoint that returns or changes data.

Diagram: the fix, server-side ownership checks The same two requests as before, but now the server also checks whether the item belongs to the user. The request for Dani's invoice succeeds; the request for Yael's invoice is rejected with a 403. D Dani (logged in) D Dani (same session) GET /invoice/1001 GET /invoice/1002 Server Checks: Logged in? ✓ Belongs to user? ✓/✗ 200 OK 403 Invoice 1001 Dani's: fine Not Authorized Request blocked
Same request exactly. This time the server checks ownership, not just login. Dani's own invoice request succeeds as normal; the request for Yael's invoice is blocked before any data leaves the server.

Note: making IDs harder to guess (say, a long UUID instead of 1001, 1002) helps a little, but it's not a real fix: that's obscurity, not authorization. A determined attacker, or an automated scanner, can still find a valid ID. And sometimes it doesn't even take any technical sophistication at all: Yael stepped away to the office kitchen for a moment and forgot to lock her screen, and Dani simply copied the secret ID that was sitting right there in front of him. And once someone already has one ID (from a shared link, a support ticket, an unlocked screen, another bug), a complicated ID won't stop them. The check itself is the only real solution, and you need it on every endpoint that returns or changes data. Forgetting it in just one place is enough to expose the whole system.

How to Check If Your App Is Exposed to This Pattern

This check takes five minutes and requires no technical knowledge:

  1. Open two different user accounts in your app (or ask a friend to open a second one).
  2. From the first account, find any private item (an order, a document, a profile) and copy its URL.
  3. Log out, and log in with the second account.
  4. Paste that exact same URL into your browser's address bar and hit Enter.

If the first account's data shows up on screen, you've found this pattern in your app. Better that you find it than someone else does.

How secureFlows Solves This From the Start

In secureFlows, access to data is determined by who's asking, not by an ID your app sends. When a user logs in, secureFlows issues a short-lived session token that securely identifies them, and every request carries that token. The server returns only what the token holder is allowed to see, no matter what number appears in the request. In other words: there's no such thing as "changing a number in the address" and getting someone else's data, because access to the data was never dependent on that number in the first place.

Think of it like a hotel key card: the key card you're given only opens your room, because the identification is encoded in the card itself. That's exactly how the token works: ownership isn't re-checked every time by someone remembering to add a line of code. It's simply built into the card that every request carries with it.

secureFlows logo

Want that check built into the infrastructure, instead of depending on a developer's memory at 2am? The "Start Building for Free" button below signs you up for secureFlows.

Start Building for Free