The release we delayed because one permission bug changed everything
A green test run, a confident sign-off, and one permission check that was enforced in the UI but not the API. We caught it the day before release — here's how, and why it changed how we sign off.
This is a case study, details blurred, about the release we held at the last minute because a single authorization bug turned out to expose every customer's data to every other customer. The suite was green. The feature demo was flawless. The bug was real, severe, and invisible to everything except one deliberate test.
Context
A B2B product adding a new "team members" view: an admin could see and manage the users in their own organisation. The UI was built so that only admins saw the management screen, and only their own org's users appeared. Functionally it worked perfectly in every demo — the right people saw the right things.
Symptoms
There were no symptoms, which is exactly the problem with this class of bug. Nothing crashed, nothing errored, every test passed, and the feature did precisely what the happy-path demo showed. The issue surfaced only because one tester, late in the cycle, decided to test the API directly instead of through the UI.
Investigation
The management screen called an endpoint like GET /api/orgs/{orgId}/members. Through the UI, orgId was always your org and the screen was hidden from non-admins, so it always behaved. The tester did two things the UI never did:
- As a non-admin user, called the endpoint directly. It returned the member list. The UI hid the screen; the API didn't check the role at all.
- As an admin of org A, changed
orgIdto org B's id. It returned org B's members — names, emails, roles. A textbook IDOR: the server trusted the id in the URL instead of checking whether you were allowed to see that org.
Both protections that everyone assumed existed lived entirely in the front end. The server enforced nothing.
Root cause
The authorization was implemented as UI logic — hide the button, scope the request to the current org — and never duplicated on the server. The frontend was treated as the security boundary, which it can never be, because anyone can call the API without it. The new endpoint shipped with no role check and no ownership check; it returned whatever it was asked for. Classic broken access control, the number-one item on every security list, hiding behind a polished UI.
What the tests missed
Every test drove the feature through the interface, as the right user, looking at their own org — the one path where the missing server-side checks didn't matter. There was no test that called the endpoint as the wrong role, and none that tampered with the resource id. The suite proved the UI behaved; it said nothing about the server, because nothing ever asked the server an impertinent question. A green run on the wrong layer is exactly why release readiness is more than a pass rate.
The reusable lesson
We held the release a day, added server-side role and ownership checks, and added the tests that should have existed. The durable change was to our sign-off: any feature that exposes or mutates data now requires an explicit authorization pass at the API layer — wrong role, wrong owner, tampered id — regardless of what the UI shows. UI-level access control is a usability nicety, never a security control. The day's delay was cheap; the same bug found a week after launch would have been a breach.
Authorization case lessons
- Test access control at the API, not only through the UI — the UI is not a security boundary
- Call privileged endpoints as a lower-privilege user; the server must refuse
- Tamper with resource ids (orgId, userId) to reach data you don't own — expect a hard no
- A green suite that only drives the happy-path UI proves nothing about server-side authorization
- Make an API-layer authz pass a sign-off gate for any feature that exposes or changes data
- Finding this pre-release costs a day; finding it post-release is a breach
// RELATED QA.CODES RESOURCES
Checklist
Common Bug
// related
How we reduced release testing from two days to four hours
Cut a dreaded two-day regression to an afternoon — and caught more — by weighting on risk, automating the stable core, and pruning dead cases.
The bug that only happened after daylight saving time changed
A case study: a scheduling bug that stayed invisible until the clocks changed — and the test scenarios that would have caught it.