DEV Community

Cover image for Three ways teams handle broken access control
aldin
aldin

Posted on Originally published at daily.dev

Three ways teams handle broken access control

Broken Access Control rose from #5 in 2017 to #1 in 2021, and it’s still #1 in the 2025 edition.
Now, compare to injection: it was #1 in 2017 and fell to #3 in 2021. In 2025 it’s #5.

OWASP ranking comparison GIF

Why one fell and the other one is still at the top?

To be clear, these are my opinions, not OWASP’s.

Injection got generic defense(s). Parameterized queries, ORMs and framework defaults made its most common forms much harder to commit by accident.

Access control can’t be solved that way, because, well... “Who may do what” is a pretty specific question to ask, and each platform will treat it differently. No library knows your rules, and no scanners can know a check is missing when they don’t know it should even exist in the first place.

So, it stays invisible until a disaster happens, and security budgets tend to follow breaches. Capital One is a good example. In 2019, an attacker got through a misconfigured firewall (widely reported as an SSRF, the same flaw that OWASP’s 2025 report merged into broken access control) and picked up the keys to that firewall’s cloud role, which opened the door to a huge amount of stored data. About 100 million people in the US and 6 million in Canada were affected, regulators fined the bank $80 million, and the board had to hand in a plan to fix its risk management.

Now, one case proves nothing, but it’s a good reminder of how much damage one overly generous identity can do.

Two caveats. Ranks are relative, so this doesn’t prove injection went away, only that it stopped being the top problem while broken access control didn’t. AND, the 2025 edition merged SSRF into this category, broadening it, but access control was already #1 in 2021.

What does OWASP recommend?

The prevention advice is to deny by default and use one centralized access-control mechanism consistently, rather than scattering logic across individual handlers.

No tool can know your rules, but they can give you a place to put rules into.

Teams tend to do this in many ways: shared libraries, roles in identity tokens, database row-level security, cloud IAM.

This piece uses an imaginary football club to cover common approaches among dedicated authZ engines.

Three ways to give the guard the facts

Think of match day. A guard at the gate has one job: decide whether each person may come in. That’s what every authorization tool does. The guard is the tool, and what differs is where the guard gets the facts. (My club is made up, so don’t check the rules against your own)

A football match ticket entry checkup

#1 Season ticket holders list

The guard has a short list:

  • Alice holds a season ticket.
  • Season tickets admit holders to league and cup matches.

Alice arrives for a league match. The guard follows the trail: Alice → season ticket → league matches. In.

A few days later she’s back for a Europa League night. Season tickets don’t cover those, so no trail leads anywhere for her now. Back to the cashier.

That’s the idea behind Google’s Zanzibar: keep small facts on a list, then follow the trail. (The real system also has a configuration language for how facts combine, but the list is the heart of it.) The technical names are relationship tuples and a graph check.

Google’s 2019 paper describes Zanzibar serving Calendar, Cloud, Drive, Maps, Photos and YouTube, scaling to trillions of access control lists and millions of requests per second.

The hard part is keeping every gate’s copy of the list current. Say Alice’s ticket is cancelled at 5:59, and the news reaches only some gates. At 6:00, a gate with the old list still lets her in. Zanzibar’s rule, enforced with a token called a zookie, is that when you ask about something that just updated, the answer must come from a list at least as new as that update.

➕ It fits nested, shared things like teams, folders and sharing links.

➖ List is the record for who’s connected to what, so your app has to keep it updated alongside its own database. If they disagree, you'll get wrong decisions.

⚙️ OpenFGA, SpiceDB, Ory Keto, Permify, Warrant

#2 A rulebook

A different guard has no list. This one has a rulebook:

  • A ticket holder can bring one child under 7 in free.
  • Gates open 90 minutes before kickoff.

Alice arrives an hour before kickoff with her season ticket and her 6-year-old. The guard checks three things: is the ticket valid, how old is the child, what time is it? All fine, so both are in.

Next match, same Alice, same ticket, but this time she brings her 9-year-old. He’s over 7, so he needs his own ticket. Same adult, different answer. And if she’d turned up 3 hours early, the gates wouldn’t be open yet. No.

The guard remembers nothing. Each answer comes from what’s in front of them right now, used once and forgotten.

In real systems, your app is the one who looks things up. It gathers the facts from its own database (the ticket, the child’s age) and hands them to the engine with every question.

➕ Rules that depend on the moment are easy to write, and your app stays the record, so there’s no copy to keep in sync.
➖ The guard only knows what they’re shown. Show the wrong age or the wrong ticket and you get the wrong answer, so your app has to pass the right facts every time. The rules also need versioning and rollout of their own.
⚙️ Cerbos, Cedar

#3 Rulebook plus a clipboard copy

This guard uses the same rulebook as Way 2, but doesn’t wait to be shown the facts. They keep a copy of the ticket office’s records on a clipboard and check against it. Decisions are fast, because everything is already in hand.

The catch is that the copy can go out of date. A fan buys a ticket at 5:59, and at 6:00 the guard’s clipboard doesn’t show it yet. Turned away.

➕ Decisions use data that’s already local, so there’s no fetching per check.
➖ The copy can lag, and something has to keep it updated.
⚙️ OPA + OPAL, Topaz (community maintained)

⚠️ A side note on OPA. It's really the guard, not the way. OPA can also work like Way 2, depending on how you feed it with facts.

⚠️ A side note on non-humans. The guards of our football example only checked people, but plenty of real visitors aren’t human (service accounts, API keys, build bots, AI agents). Three ways we covered work for them too, as the guard doesn’t care who’s asking for access. What differs is cleanup. When an employee leaves, someone switches their access off. When a bot’s project ends, nobody does, so its access often just keeps working.

OWASP’s separate Top 10 for non-human identities puts exactly this at #1, under the name improper offboarding.

So what now?

None of the three listed ways knows your rules. You still have to write those.

What differs is where the guard looks up the facts: a list, what’s shown at the gate, or a copy kept at hand. So the choice is really about which cost you’d rather carry:

  • keeping a list in sync,
  • passing the right facts with every check, or
  • living with a copy that can lag.

None of the tools works exactly the same, so look into the specific implementations and trade-offs when choosing.

But the exact choice matters less than making a choice at all.

I’d argue access control keeps turning up in so many applications because nobody has to answer “who may do what?” until something breaks, and, budgets tend to arrive after the breach.

Put the decision in one place, deny by default, and have the conversation while it’s cheaper.

Top comments (0)