#software-development
#product
#founders
Security

I changed one number in the URL and saw another customer's orders

A client asked me for a second opinion on the app another shop had shipped. Twenty minutes in, changing one number in the address bar, I was reading another company's order on his platform. Login worked; authorization didn't. On the cheapest hole to prevent and the most expensive to find late.

Por Victhor Araújo

Founder of Revin. Engineer by training, specialist in software development and digital products.

The client came in through a referral, asking for a second opinion. The previous shop had shipped his app about four months earlier. It was live, taking orders, and he just wanted to know whether the house was in order before signing the next phase with the same crew.

I opened the dashboard on a test account and looked at the address bar on an order page. It ended in something like /orders/10432. I changed the number to 10431 and hit enter. Up came another company's order on his platform: name, total, line items, contact email. No error, no warning. A clean 200, as if it were mine to read.

If you pay a vendor to build your product and nobody has ever shown you how one customer's access is kept separate from another's, this one is for you.

A second-opinion finding almost never shows up in the demo. It shows up when someone sits down and tests what the app does with an order that isn't theirs.

A second-opinion finding almost never shows up in the demo. It shows up when someone sits down and tests what the app does with an order that isn't theirs.

Being logged in is not the same as being allowed

The bug has a name, and it has topped application-security lists for years: broken access control. In plain terms, the system checked who you are at login and stopped there. When it handed over that specific order, it never asked again whether the order was yours.

Two different questions, and they are easy to blur, because the first one clears the login screen and quiets most manual testing. Authentication answers "are you Victhor?". Authorization answers "is this particular order Victhor's?". The shop handled the first with care: token, expiry, all in place. The second, they skipped.

On a senior squad that second question belongs in the endpoint's definition of done, not in an audit six months later. That is the gap between treating authorization as a layer of the architecture and treating it as a detail you can leave for the end. And the end, spoiler, never arrives.

Why this hole shows up in almost every app we inherit

The cause is almost always one innocent line of code. The dev takes the id from the URL and asks the database: "give me order 10431". The query works, the screen renders, the demo passes. What was missing is a second filter: "give me order 10431 that belongs to this user". One clause. That is the size of the hole.

But it shows up nowhere when you glance at the product from above. The customer sees his own order, all fine. Nobody, in a normal flow, types someone else's id. The problem only exists for whoever goes looking, and a vendor paid by feature delivered has no reason to go looking. Isolating one customer from another never becomes a pretty screenshot at the end of the sprint.

Keeping one customer separate from another is an architecture decision, drawn on paper before the first endpoint, not a patch bolted on after the data already leaked.

Keeping one customer separate from another is an architecture decision, drawn on paper before the first endpoint, not a patch bolted on after the data already leaked.

23 of 61 endpoints, and nobody had noticed

In 2024 we took over a logistics platform with exactly this pattern. It had been running for about two years. When we mapped the endpoints, 23 of the 61 that returned customer data checked for no owner at all: being logged in was enough. Any account could list any other account's deliveries by swapping an id. As far as we could reconstruct from the logs, nobody had exploited it. But "nobody has exploited it yet" and "it is safe" are very different sentences.

That 23-of-61 may not be representative, and I'll own the bias: we get called precisely for the projects that already smell like trouble, so my sample leans toward chaos. Even so, on the apps that arrive relatively healthy, the authorization inventory rarely comes back with zero gaps on the first pass. The pattern repeats too often to be bad luck.

How we close it by default 🔒

The rule Revin applies with every client is boring precisely because it is simple: no endpoint that takes an id ships to production without a test that tries to read another owner's data and demands a 403 in return. Not the happy-path test, the malicious-path one. And it lives in CI, runs on every pull request, and depends on nobody remembering.

  • Owner filter on the query: every read query carries the user, never just the resource id.
  • A negative test in CI: for each endpoint, a case that attempts the wrong access and expects a 403 or 404.
  • One central policy layer: the "can they see this?" decision lives in a single place, not scattered across controllers where someone forgets.

If you can't say how many of your endpoints check ownership today, that is exactly the kind of thing we open up in a 30-minute diagnostic: https://revin.com.br/en/diagnostic-sprint. It takes less time than the meeting where the problem turns into a crisis.

What I told that client

I didn't tell him to sue the previous shop or to rebuild from scratch. Rebuilding from scratch is almost never the answer, and that is a story for another day. I told him to stop signing the next phase with a crew that treated access separation as optional, and to require, from whoever stayed on, an authorization test per endpoint in the pipeline. It is verifiable. Either it is running there, or it isn't.

The good part of this problem is that closing it costs little once someone decides to look. Finding out late, with a customer asking why they saw the neighbor's data, costs the rest. If you want an honest read on how your access is separated today, the conversation starts here: https://revin.com.br/en/schedule-a-call.

Ready to elevate your business

Schedule a meeting
Share
Link de compartilhamento LinkedinLink de compartilhamento XLink de compartilhamento WhatsappLink de compartilhamento Facebook

Every two weeks. The technical decisions we made, and what we learned.