Cates Works
All notes
Security/ 7 min read

Permission bugs don’t throw errors. They just leak.

July 29, 2026

Most software defects announce themselves. A page 500s, a form refuses to submit, a customer emails you a screenshot. Access-control defects are different: when a permission check is wrong, the system does exactly what it was asked to do, quickly and successfully, and hands the data to the wrong person. Nothing logs an error, because from the software’s point of view nothing went wrong.

What an honest audit found

I ran a deliberate access-control audit across my own platform — the kind you run before customers exist rather than after. It was not a comfortable exercise, and the findings are worth naming plainly, because they are the same few shapes that show up in nearly every business application I have ever reviewed.

  • Handlers that trusted an account identifier sent in the request instead of the one proven by the signed-in session — the classic way one customer reads another customer’s records.
  • Approval steps where the same person could both request and confirm a sensitive change, defeating the entire point of requiring two people.
  • Credentials stored without encryption when the encryption key was simply absent, rather than the system refusing to store them at all.
  • A directory of staff information readable by anyone who could sign in, rather than by the people whose job requires it.
  • Six separate places where a missing or unreadable role was treated as permission granted instead of permission denied.

Fail closed, always

That last one has a name: failing open. It happens when the code asks “does this person have the required role?”, gets back nothing at all — no role, an unreadable token, a service that didn’t respond — and interprets the silence as yes. It is almost never a decision anyone made on purpose. It is what you get when the unhappy path was never written down.

The rule that closes it is boring and absolute: absence of proof is denial. No role means no. An unreadable claim means no. A caller with no organisation attached means no. If the system cannot positively establish that you are allowed to do this, the answer is no — and the way you know it’s enforced is that the missing case is a compile-time error, not a runtime guess.

Why this matters more as you add tools

Every business app you add — a booking system, a client portal, a staff dashboard, a mobile app — brings its own idea of who is allowed to do what. Five apps with five slightly different permission models is five chances to get it wrong, and no way to answer a simple question like “what can this contractor actually see?” without opening five admin screens.

The remedy is one shared definition of roles, in one place, that every application reads from — so the ladder from viewer to contributor to admin means the same thing everywhere, and revoking access once actually revokes it everywhere.

What to ask your own developer

  • When a request arrives, where does the account identifier come from — the URL, or the verified session?
  • What happens when the permissions service is unreachable? Is the answer “allow” or “deny”?
  • Can the same person approve their own sensitive change?
  • When someone leaves, how many systems does someone have to remember to revoke?
  • What happens to stored secrets if the encryption key isn’t configured — does the system refuse, or shrug?

The takeaway

Access-control bugs return 200 OK, so no alarm will ever tell you about them. Assume nothing until it’s proven, deny by default, and define roles once for every app instead of once per app.

Let’s talk

Have a project, or a product that could work harder?

Most projects begin with a short, no-pressure discovery call.