• Agents
  • Pricing
  • Blog
Log in
Get started

Security for apps built with AI. Paste a URL, get a report, fix what matters.

Product

  • How it works
  • What we find
  • Pricing
  • Agents
  • MCP Server
  • CLI
  • GitHub Action

Resources

  • Guides
  • Blog
  • Docs
  • OWASP Top 10
  • Glossary
  • FAQ

Security

  • Supabase Security
  • Next.js Security
  • Lovable Security
  • Cursor Security
  • Bolt Security

Legal

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • Imprint
© 2026 Flowpatrol. All rights reserved.
Home/OWASP Top 10/API Top 10/API1: Broken Object Level Authorization
API1CWE-284CWE-639

Your API trusts the ID in the request
Broken Object Level Authorization

The number one API bug: endpoints that hand out any object to anyone who asks for it by ID.

The #1 API vulnerability in the OWASP 2023 list — found in roughly 40% of tested APIs.

Reference: API Top 10 (2023) — API1·Last updated April 7, 2026·By Flowpatrol Team
Broken Object Level Authorization illustration

Your mobile app looks locked down. The buttons only show what the user is allowed to see. But the API underneath doesn't know that — it just answers whatever comes in. An attacker skips the app entirely and talks to the API directly, one object ID at a time.

BOLA is the API twin of IDOR. Every API endpoint that fetches an object by ID has to ask two questions: does this caller have a session, and does this specific object belong to them? Most generated APIs only ask the first. The second is the authorization the attacker walks through.

What your AI actually built

You asked for a mobile backend, a GraphQL layer, or a REST API to power a SPA. The model delivered a clean set of resource routes: GET /api/orders/{id}, GET /api/documents/{id}, and a tidy GraphQL node(id:) resolver that looks up anything by global ID.

The UI only ever calls those endpoints with IDs the current user owns, so during testing everything behaves. You never saw the shape of the bug because you never typed another user's ID into a URL.

The API itself has no idea which objects belong to which account. Ownership isn't enforced at the resolver, the route, or the database. The ID is the entire authorization.

How it gets exploited

An attacker installs the mobile app, creates a real account, and points it at a local proxy like mitmproxy or Burp.

1
Capture the contract
They tap around the app for five minutes and watch every request fly past the proxy. The API shape writes itself: /api/v2/orders/{id}, /api/v2/messages/{id}, /api/v2/uploads/{id}.
  • 2
    Swap the ID
    They replay their own request with a different numeric ID. The server returns somebody else's order — shipping address, last four of the card, the works.
  • 3
    Script the range
    A tiny loop iterates 1 through 500000. Every response is a real customer. GraphQL makes it even easier: one query with an array of node IDs returns them all in a single round trip.
  • 4
    Move laterally
    The same trick works on /api/v2/uploads — signed URLs to private documents. And on /api/v2/messages — private DMs. The bug isn't in one route. It is the whole API.
  • The attacker dumps the entire production dataset through the official API, using a legitimate session. Nothing in the logs looks unusual — just a paying customer asking for records, many records, very quickly.

    Vulnerable vs Fixed

    Vulnerable — GraphQL node resolver with no ownership check
    // graphql/resolvers/order.ts
    export const orderResolvers = {
      Query: {
        order: async (_parent, { id }, ctx) => {
          // ctx.user exists — they're logged in. Good enough, right?
          return ctx.db.order.findUnique({
            where: { id },
            include: { items: true, shippingAddress: true },
          });
        },
      },
    };
    Fixed — ownership filter in the query itself
    // graphql/resolvers/order.ts
    export const orderResolvers = {
      Query: {
        order: async (_parent, { id }, ctx) => {
          if (!ctx.user) throw new GraphQLError('Unauthorized');
    
          const order = await ctx.db.order.findFirst({
            where: {
              id,
              accountId: ctx.user.accountId, // authorization lives in the where clause
            },
            include: { items: true, shippingAddress: true },
          });
    
          if (!order) throw new GraphQLError('Not found');
          return order;
        },
      },
    };

    Authentication proves who you are. Authorization proves this specific row is yours. The fix is not a middleware — it is a clause in the database query that ties the object to the session. Never return 403 here; a 404 avoids confirming the object exists.

    A real case

    Optus leaked 9.8 million customer records through an open API endpoint

    In 2022, an unauthenticated API endpoint on an Optus subdomain returned customer records by sequential ID. No exploit, no zero-day — just BOLA at the edge of the public internet.

    Related reading

    Glossary

    Broken Object Level Authorization (BOLA)Insecure Direct Object Reference (IDOR)RLS in Supabase & PostgreSQL (Row Level Security)

    What we find

    broken access control

    From the blog

    supabase rls the security feature your ai forgotlovable rls vulnerability 170 apps exposed

    References

    • API1: Broken Object Level Authorization — official OWASP entry
    • OWASP API Security Top 10 (2023) — full list
    • CWE-284 on cwe.mitre.org
    • CWE-639 on cwe.mitre.org

    Find every BOLA bug in your API.

    Flowpatrol replays your real endpoints across real user sessions and proves which objects cross tenants. No config, no agent, no SDK.

    Try it free