HackproofHacks
Web App Security 15 min read

IDOR and Broken Access Control: Finding and Fixing the #1 Web Vulnerability

Broken access control is the most common serious web vulnerability, and IDOR is its most reported form. Learn how these access-control flaws work, how testers find them, and how to fix them properly with server-side authorisation.

Hassan Ansari

Hassan Ansari

A code card showing an IDOR request retrieving another user’s invoice by id

IDOR and broken access control: the #1 web vulnerability, explained

If you test web applications for a living, or you’re learning to, you’ll find broken access control more often than any other serious issue. It has sat at the top of the OWASP Top 10 since 2021 (see the official OWASP Top 10 project for the underlying data), appearing in the large majority of tested applications, and its most common form, IDOR, is among the most reported vulnerabilities in bug bounty programs.

The reassuring news is that access-control flaws are conceptually simple. The uncomfortable news is that they’re everywhere, because the fix requires discipline applied consistently across an entire codebase. This guide covers how they work, how to find them, and how to fix them properly.


Authentication vs authorisation: the distinction that matters

Almost every access-control bug traces back to confusing two ideas:

  • Authentication is proving who you are. Logging in. Frameworks handle this well.
  • Authorisation is checking what you’re allowed to do. Whether this specific authenticated user may access this specific resource. This is custom logic, and it’s where things break.

Broken access control is an authorisation failure. The user is correctly authenticated, they really are logged in as themselves, but the application never properly checks whether they’re allowed to perform the action they’re attempting. It authenticates and then trusts.


What is IDOR?

IDOR (Insecure Direct Object Reference) is the most common form of broken access control. It happens when an application exposes a reference to an internal object (a database record, a file, an account) and fails to verify that the requesting user is authorised to access that particular object.

The classic example is an identifier in a URL:

GET /api/v1/invoices/1042  → returns your invoice
GET /api/v1/invoices/1043  → returns someone else's invoice

Change one number and you get another user’s data, with no elevated privileges and no clever payload required, just an enumerable identifier and a missing ownership check on the server. That’s IDOR in its purest form.

It applies to any action, not just reading:

GET    /api/v1/orders/5001        → view an order that isn't yours
POST   /api/v1/orders/5001/cancel → cancel an order that isn't yours
DELETE /api/v1/files/8830         → delete a file that isn't yours

Whenever the server acts on an object based purely on an identifier the user supplied, without confirming ownership, you have IDOR.


Horizontal vs vertical: the two directions of escalation

Access-control failures let a user reach things they shouldn’t, in one of two directions.

Horizontal privilege escalation

Accessing the data of another user at the same privilege level. User A reads User B’s invoices, messages, or profile. The invoice example above is horizontal escalation. In API terms this is BOLA (Broken Object Level Authorisation), the number-one risk on the OWASP API Security Top 10, because modern apps address every object by ID.

Vertical privilege escalation

A lower-privileged user reaching higher-privileged functionality. A normal user performing admin actions. The obvious /admin panel is usually protected, but the forgotten API variant often isn’t:

GET /admin/users        → 403, correctly blocked
GET /api/admin/users    → 403 as well... usually
GET /api/v1/admin/users → this route variant was never protected

Different URL structures pass through different middleware stacks. If authorisation is applied per-route instead of per-resource, gaps like this are almost guaranteed to appear as an application grows.


Broken access control is broader than IDOR alone. Watch for:

  • Forced browsing: reaching pages or endpoints by guessing their URL, relying on them being “unlinked” rather than protected.
  • Mass assignment or parameter tampering: adding an unexpected field to a request, like "role": "admin" or "is_verified": true, that the server blindly binds to the object.
  • Missing function-level access control: the client hides an admin button, but the underlying endpoint has no server-side check, so anyone who calls it directly succeeds.
  • Metadata manipulation: tampering with a JWT, cookie, or hidden field that encodes the user’s role or identity.

They share one root cause: trusting the client instead of enforcing authorisation on the server.


How testers find access-control bugs

Finding IDOR is less about tools and more about careful observation, the same slow, question-driven hunting mindset that separates good testers from scanner operators. The reliable method uses two accounts:

  1. Create two users, call them A and B, ideally with some data in each.
  2. Act as User A with a proxy like Burp Suite capturing every request. Note every request that references an object by ID: invoices, orders, messages, files, profile fields.
  3. Replay A’s requests using B’s session. Take a request that legitimately accessed A’s object, swap in B’s session token, and keep A’s object ID. Does the server return A’s data to B? If yes, that’s a broken object-level authorisation.
  4. Enumerate and tamper. Try incrementing and decrementing IDs. Try adding privileged fields to write requests. Try the API variants of protected admin routes.
  5. Test every verb. An object might be readable only to its owner but deletable by anyone who knows the ID. Check GET, POST, PUT, PATCH, and DELETE independently.

The tell-tale sign is always the same: the server acts on an object based on the ID you supplied without confirming you’re allowed to touch it.


Why UUIDs don’t save you

A common misconception: “we use random UUIDs instead of sequential IDs, so we’re safe from IDOR.” You’re not.

Random identifiers make objects harder to discover by guessing, since an attacker can’t simply count from 1042 to 1043. But that’s obscurity, not access control. If the ownership check is missing, an attacker who obtains a valid identifier through any channel, a shared link, an API response that lists IDs, a Referer header, a mobile app, server logs, a support ticket, can still access the object.

UUIDs are a reasonable defence-in-depth measure that raises the effort of enumeration. They are never a substitute for the real fix.


How to fix broken access control

The fix is simple to state and requires discipline to apply everywhere.

1. Enforce authorisation on the server, for every request

Every request that accesses a resource must verify, on the server, that the authenticated user is permitted to access that specific resource. Not “is the user logged in,” but “does this user own, or have explicit permission for, this exact object.”

def get_invoice(invoice_id, current_user):
    invoice = db.get_invoice(invoice_id)
    if invoice is None or invoice.owner_id != current_user.id:
        raise Forbidden()          # deny by default
    return invoice

The ownership check, invoice.owner_id != current_user.id, is the entire fix. It must exist on every path that touches the object.

2. Deny by default

Design the system so that access is denied unless explicitly granted. A forgotten endpoint should fail closed, not open. New routes should inherit “no access” and require a deliberate grant, so the failure mode of forgetting a check is a locked door, not an open one.

3. Centralise the check where it can’t be bypassed

Applying authorisation ad hoc in each controller guarantees inconsistency. Enforce it at the data-access layer or through a shared policy mechanism, so the same object can’t be reached through a different route that forgot the check. The more central the enforcement, the fewer gaps.

4. Never trust the client

Hiding a button, disabling a field, or omitting a link is a UI convenience, not a security control. Every one of those actions must be re-checked on the server. Assume the attacker crafts requests by hand, because they do.

5. Reject unexpected fields

Defend against mass assignment by explicitly binding only the fields a user is allowed to set. If a request includes role or owner_id and the user has no business setting them, reject or ignore them rather than binding them to the object.


Key takeaways

  • Broken access control is the most common serious web vulnerability, and IDOR is its most reported form.
  • It’s an authorisation failure: the user is authenticated, but the server never checks whether they’re allowed to access the specific resource.
  • Escalation runs horizontally (other users’ data, BOLA in APIs) and vertically (reaching admin functionality).
  • UUIDs don’t fix it. Only a server-side ownership check does.
  • Fix it by enforcing authorisation on the server for every request, denying by default, centralising the check, and never trusting the client.

Access-control bugs persist because fixing them takes discipline applied to every endpoint, forever, not a library you install once. Understand it well and you’ll both find these bugs as a tester and prevent them as a builder.

For the defensive patterns in code, the OWASP Authorization Cheat Sheet is the reference worth keeping open, and PortSwigger’s free access control labs give you a legal target to practise every variant described above.

Keep going: read the OWASP Top 10 breakdown, the API security testing guide, or start with the bug bounty beginner’s roadmap. When you’re ready for structured, hands-on learning, explore our training.

#IDOR #broken access control #BOLA #OWASP #authorisation #API security
Free newsletter

Liked this? I write one like it every week.

One practical security lesson in your inbox each week, explained the same simple way. Join 10,000+ readers. Unsubscribe anytime.

From the article

Need a security assessment?

HackproofHacks provides web application and API penetration testing — using the same techniques covered in this article, with your explicit authorisation.

Book a free scoping call

More on Web App Security.

All articles →
FAQ

Questions about this topic.

What is IDOR (insecure direct object reference)?

IDOR is a vulnerability where an application exposes a reference to an internal object — like a database ID in a URL — and fails to check whether the current user is actually allowed to access that object. If a page loads your invoice at /invoices/1042 and you can simply change the number to /invoices/1043 and see someone else's invoice, that's IDOR. The reference is 'direct' because it maps straight to the underlying object, and it's 'insecure' because the server trusts the reference without verifying ownership.

What is the difference between IDOR and broken access control?

Broken access control is the broad category: any failure of the application to enforce what a user is allowed to see or do. IDOR is one specific, extremely common form of it — accessing objects you don't own by manipulating their identifiers. Broken access control also covers things like reaching admin functionality as a normal user, bypassing workflow steps, and forced browsing to hidden pages. IDOR is the most frequently reported member of the family, but they're fixed with the same core principle: enforce authorisation on the server for every request.

What is BOLA and how does it relate to IDOR?

BOLA stands for Broken Object Level Authorisation, and it's essentially IDOR in the context of APIs. It's the number-one risk on the OWASP API Security Top 10. When an API endpoint like GET /api/orders/{id} returns an order without checking that the order belongs to the authenticated caller, that's BOLA. Because modern applications are built on APIs where every object is addressable by ID, BOLA is the most prevalent and impactful API vulnerability, and it's found constantly in real testing.

Do UUIDs prevent IDOR?

No. Using long, random identifiers like UUIDs instead of sequential numbers makes IDOR harder to discover by guessing, but it does not fix it. This is security through obscurity, not access control. If the true authorisation check is missing, an attacker who obtains a valid identifier — from a shared link, a referrer header, an API response, a mobile app, or a logged URL — can still access the object. UUIDs raise the effort slightly; they do not replace a server-side ownership check.

How do you fix IDOR and broken access control?

Enforce authorisation on the server for every single request that accesses a resource. For each request, verify that the authenticated user actually owns or is permitted to access the specific object being requested — not merely that they're logged in. Deny by default, so any request that isn't explicitly allowed is rejected. Apply the check at the data-access layer where it can't be bypassed by a different route or a forgotten endpoint, and never rely on the client hiding a button or on identifiers being hard to guess.

Why does broken access control top the OWASP Top 10?

Because it's both extremely common and hard to prevent systematically. OWASP data has shown it present in the vast majority of tested applications. Authentication — proving who you are — is largely handled by frameworks, but authorisation — checking whether that authenticated user may perform a specific action on a specific resource — is custom logic that developers write themselves, differently in every controller and endpoint. That inconsistency means checks get forgotten, especially on newer API routes and edge cases, which is exactly where access-control bugs appear.