The API had already been through a round of testing. Someone tried the obvious thing first: change the ID in GET /api/orders/1837 to a number that belongs to another customer and see what comes back. It came back with a 403. Good, the team had wired in an object-level check on that endpoint, does the requesting user actually own this order, and it worked exactly as designed. Broken Object Level Authorization, OWASP's API1:2023, the single most common API vulnerability class, tested and blocked.
Then someone pointed the same account at the GraphQL endpoint sitting next to that REST API, serving the same underlying data for the newer mobile client, and asked a different kind of question. Not "can I ask for someone else's order directly." Instead: order(id: 1837) { customer { orders { id, total, items } } }, starting from an order the account genuinely owns, then walking sideways through the customer relationship to that customer's full order list. The top-level order resolver still ran its ownership check, id 1837 belongs to this account, request approved. But the orders field hanging off customer is a different resolver entirely, and nobody had wired an ownership check onto it, because nobody thought about it as a place data could leak from. It just walks the relationship and returns whatever it finds. Every order belonging to that customer came back. If that customer object connects to anything else, an organization, a linked account, a shared address book, the same trick usually keeps working one hop further.
This is not a GraphQL bug in the sense of broken software. GraphQL is doing exactly what it's built to do: let a client traverse relationships between types instead of hitting one flat endpoint per resource. That's the entire value proposition. But it means "authorization" can't be a property of an endpoint anymore, because there often isn't a fixed set of endpoints, there's a schema, and any resolver that returns object data is a place someone can arrive at from more than one direction. A REST API gets its authorization boundary for free, roughly, because each URL only does one thing. A GraphQL API has to reimplement that boundary at every single resolver that touches sensitive data, deliberately, because the framework has no built-in notion of "this field needs the same ownership check as that other field." OWASP's own GraphQL guidance says this directly: authorization has to live in the business logic layer and be applied per resolver, not assumed from the shape of the schema or the fact that some other field already checked.
What makes this worth specifically testing for, separate from ordinary IDOR testing, is that the team can do everything right at the entry point and still ship the hole. The top-level query was reviewed, the ownership check was code-reviewed, it might even have a test. The nested field three hops away that returns the same data through a different name almost never gets that same attention, because it doesn't look like "the orders endpoint," it looks like a field on a type. Testing it means picking every relationship in the schema that leads back to sensitive data and walking it from an account that shouldn't have access, not just re-testing the fields with the obvious names.
This is exactly the kind of authorization-boundary thinking the API Hacking Book is built to drill, BOLA, JWT and OAuth abuse, SSRF and GraphQL abuse specifically, across 40 chapters of hands-on labs instead of a single checklist item: https://resources.codelivly.com/product/hacking-your-first-apis/
The free IDOR and JWT labs and the HelpHub IDOR / Broken Access Control CTF on codelivly.com are the same object-level-authorization muscle, hands-on, before you spend anything: https://codelivly.com/labs/idor and https://codelivly.com/ctf/challenges/helphub-idor
Top comments (0)