Chronoloq
    Blog & Threat Research
    API SecurityMoaz Ehab April 12, 2026 7 min read

    Broken Object Level Authorization: The Bug That's Been #1 on OWASP's List Since 2019, in Actual Code

    BOLA has held the top spot on the OWASP API Security Top 10 in every edition since 2019. It drove both the Optus breach (2.1M records) and T-Mobile breach (37M customers). Here's the exact vulnerable code pattern, the one-line fix, and the OPA gateway policy that catches it when application code doesn't.

    Broken Object Level Authorization (BOLA) has held the top spot on the OWASP API Security Top 10 in every edition since the list was first published, and it is still the mechanism behind some of the largest API breaches on record. That's not because it's a hard vulnerability to understand. It's a one-line fix, conceptually. It keeps happening because it's an easy line to forget, and nothing in a typical API stack catches the omission by default.

    What BOLA actually is

    An API endpoint that accepts an object identifier, like a user ID, invoice ID, or order ID, has to verify that the *authenticated caller* actually owns or has rights to that object, not just that the object exists. BOLA is what happens when that check is missing: the API trusts the ID in the request and returns (or modifies) whatever object matches it, regardless of who's asking.

    Two breaches make the pattern concrete. In the 2022 Optus breach, a coding error had broken access controls on a customer-facing API endpoint; the fix was applied to the main domain in 2021, but a forgotten, internet-facing legacy domain running the same vulnerable code was never patched. In September 2022, an attacker exploited it by iterating over sequential customer IDs, no advanced tooling required, and exfiltrated records for at least 2.1 million account holders, including roughly 150,000 passport numbers and 50,000 Medicare numbers. Australia's communications regulator later confirmed the attack "was not highly sophisticated" and amounted to "a simple process of trial and error" (SecDim, 2022).

    The T-Mobile breach disclosed in January 2023 followed the same shape at larger scale: an improperly secured API endpoint, apparently exposed since November 2022, allowed access to the personal data of roughly 37 million customers (Salt Security, 2023). BOLA (also known as Insecure Direct Object Reference, or IDOR, in traditional web security terms) is "responsible for the majority of large-scale API data breaches," according to Salt Security's analysis of the incident. Salt Labs' most recent State of API Security report found BOLA and injection flaws together accounted for 37% of the issues its researchers found in production APIs.

    The bad code

    Here's the pattern, in a typical Node.js/Express handler, that produces exactly the Optus and T-Mobile failure mode:

    javascriptcode
    // BAD: trusts the :invoiceId param, never checks who owns itapp.get('/api/invoices/:invoiceId', authenticate, async (req, res) => {  const invoice = await db.invoices.findById(req.params.invoiceId);  if (!invoice) {    return res.status(404).json({ error: 'Invoice not found' });  }  // Missing: does req.user actually own this invoice?  return res.json(invoice);});

    authenticate confirms the caller is *someone*. It does nothing to confirm the caller is *allowed to see this specific invoice*. Any authenticated user who increments invoiceId in the URL gets every other customer's invoice back, one request at a time. This is the exact mechanism the Optus attacker used against sequential customer IDs.

    The fix

    The correction is an explicit ownership check, scoped to the authenticated identity, before the object is ever returned:

    javascriptcode
    // GOOD: verifies the authenticated user owns the requested objectapp.get('/api/invoices/:invoiceId', authenticate, async (req, res) => {  const invoice = await db.invoices.findOne({    id: req.params.invoiceId,    accountId: req.user.accountId, // scoped to the caller, not just the ID  });  if (!invoice) {    // Same 404 whether the invoice doesn't exist or isn't the caller's.    // Don't leak existence of objects the caller doesn't own.    return res.status(404).json({ error: 'Invoice not found' });  }  return res.json(invoice);});

    Two details matter beyond the ownership filter itself. First, the query does the check rather than fetching the object and checking it in application code afterward, a filtered query is harder to accidentally bypass than an if statement someone might refactor away later. Second, the 404 response is identical whether the invoice doesn't exist or simply isn't the caller's, which avoids leaking which object IDs are valid to an attacker probing the range.

    Defense in depth: catching it at the gateway when the code doesn't

    Application-level fixes are necessary but not sufficient. This is exactly the class of bug that keeps recurring across teams and endpoints, which is why it belongs at a policy layer that doesn't depend on every engineer remembering the check on every route. Below is the same failure mode expressed as an Open Policy Agent (OPA) authorization policy, first without an ownership constraint, then with one.

    regocode
    # BAD: authorizes any authenticated request to the invoices endpoint,# with no relationship check between the caller and the resource.package api.authzdefault allow = falseallow {  input.method == "GET"  startswith(input.path, "/api/invoices/")  input.token.valid == true}
    regocode
    # GOOD: requires the resource's owning account to match the caller's# account claim before the request is authorized.package api.authzdefault allow = falseallow {  input.method == "GET"  startswith(input.path, "/api/invoices/")  input.token.valid == true  input.resource.accountId == input.token.claims.accountId}

    The second policy requires the gateway (or a sidecar like Envoy with an OPA filter) to resolve input.resource.accountId for the object being requested before authorizing the call, which means object ownership becomes infrastructure-enforced rather than something that lives entirely in application code an individual developer could get wrong. It's not a replacement for the application-level fix. It's a second, independent place the same class of bug has to fail before it becomes a breach.

    Why "forgotten endpoints" make this worse

    The Optus breach specifically involved a legacy domain that had been patched on the main API but not on a duplicate, forgotten deployment still reachable from the internet. That's not an application-logic problem, it's an inventory problem: a fix existed, and an unmonitored copy of the vulnerable endpoint outlived it. BOLA remediation only works if it reaches every live instance of the endpoint, and teams generally can't patch, or write authorization policy for, an API they don't know is running.

    This is where shadow API discovery becomes part of BOLA defense rather than a separate concern. Chronoloq's endpoint discovery is built to find exactly the kind of forgotten, undocumented, or duplicate deployment that let the Optus vulnerability persist for years after the "real" fix shipped, and its compliance reporting maps object-level authorization gaps to the OWASP category directly, so the finding reaches an engineering backlog instead of sitting in a scan report nobody reads.

    A short checklist

    1. 1Every object-returning endpoint needs an explicit ownership or relationship check, scoped by the query itself, not by a downstream if.
    2. 2Return identical error responses for "doesn't exist" and "not yours," to avoid confirming valid object IDs to an attacker.
    3. 3Push the same check to a policy layer (OPA, API gateway ACLs, a service mesh authorization filter) so one missed line in application code isn't the only thing standing between a customer and every other customer's data.
    4. 4Inventory before you authorize. A correct authorization check on a forgotten, duplicate, or undocumented endpoint doesn't help if that endpoint was never fixed in the first place.
    5. 5Test with sequential and adjacent IDs specifically. Both Optus and T-Mobile were exploited with straightforward ID enumeration, not sophisticated tooling, that's exactly what automated BOLA testing should target first.

    BOLA isn't a hard bug. It's an easy one to omit, at scale, across every endpoint an API ships, which is precisely why it's stayed at #1 since OWASP started ranking API risks.


    Chronoloq

    See the vulnerabilities covered in this research, in your own APIs

    Chronoloq scans for OWASP API Security Top 10, OWASP LLM Top 10, and agentic vulnerabilities including MCP, A2A, and RAG attack surfaces.

    Book a security review

    This website uses cookies to enhance user experience and to analyze performance and traffic on our website. We also share information about your use of our site with our social media, advertising and analytics partners. View Cookie Policy