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.
- 1Capture the contractThey 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}.
- 2Swap the IDThey 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.
- 3Script the rangeA 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.
- 4Move laterallyThe 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
// 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 },
});
},
},
};// 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
References
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