October 28, 2026
GraphQL Authorization Flaws: How One Missing Check in a Resolver Exposes Private Data
The REST endpoint said no. The GraphQL resolver never asked the question at all.

By Muhammad Asim Shahzad
5 min read
GraphQL feels modern and safe because every request is authenticated and every query is shaped by a schema. None of that guarantees authorization. A schema describes what data exists. It does not decide who may see it. That decision lives in every single resolver, and the findings in this article come from resolvers where nobody made it.
Disclosure: This article was researched and written with AI assistance, and it was checked for accuracy and originality before publishing.
By the end you will know why GraphQL authorization fails in a structurally different way from REST, how disclosed HackerOne cases prove the pattern, how to test with two accounts you own, and how to build authorization so a new field cannot quietly leak private data.
The bug class, explained simply
In a REST API, each route can carry its own access check, and a missing check creates a classic broken access control bug on that route. In GraphQL there is usually one endpoint and many resolvers behind it. Each field you can query is resolved by its own piece of code. If authorization is expected inside resolvers, then every resolver is a place where the check can be forgotten.
A public technical review of GraphQL security collects the key disclosed cases in one place and makes exactly this point. You can read it at https://dev.to/rxkov/graphql-security-introspection-injection-and-authorization-59dl
That review cites HackerOne report 489146, where the HackerOne platform itself exposed confidential information about private programmes through a GraphQL path to users who had no access to those programmes. The users were legitimate. The query was valid. The resolver simply returned data outside their scope.
It cites HackerOne report 885539, where private list membership was exposed through a GraphQL field that did not verify access, even though the equivalent REST path had the check. It also cites HackerOne report 614355, where a private namespace could be read through a GraphQL resolver by any authenticated user while the REST route for the same data enforced permissions correctly. Same data, two paths, two different answers. That contrast is the signature of this bug class.
Introspection belongs in this picture as recon only. If a schema is exposed, it helps a hunter map fields and relationships quickly. Introspection alone is rarely the paid finding. The paid finding is private data or a private action reached through a resolver that never checked access.
How the bug was found
The reliable method uses two accounts you own, with different access levels and clearly seeded private objects.
First, map the graph. Use the normal application as each account and note the queries and mutations each role is meant to use. Where introspection is available in an authorised test, use it to list fields you have not seen in the interface, especially admin, export and relationship fields.
Second, replay queries across accounts. Take a query that returns a private object for Account A and send the same query as Account B, changing only the object identifier. A correct system refuses or returns nothing. A flawed resolver returns Account A data to Account B.
Third, test fields, not only whole objects. A top level query may check access while a nested field does not. A public profile may expose a private email through a relationship field. A list you own may let you traverse into members, payment details or private notes of others because the nested resolver assumed the parent had already checked. Test the edges of the graph, because that is where checks are most often missing.
Fourth, compare GraphQL with REST when both exist. If the REST route refuses your second account and the GraphQL query allows it, you have clean comparative proof that the flaw is a missing resolver check, not intended behaviour.
Fifth, test mutations with the same care, using only objects your test accounts created. Reading private data is serious. Changing or deleting another account object through a mutation is more serious still, and it must only ever be proved on your own seeded data.
Impact, in plain words
Impact follows the field. A resolver that leaks private programme details, private list members or private project namespaces breaks confidentiality for data that owners deliberately kept closed. Relationship traversal can scale that leak, because one owned object can become a bridge to many objects you do not own.
Write the report around the data, not the technology. Name the private object, name the account that should never see it, show the two account proof, and state the business harm in plain words. Triage teams understand exposed private projects immediately. They should not need to decode GraphQL jargon to feel the impact.
How to find it and how to fix it
For hunters, keep a simple matrix. Rows are objects and fields. Columns are your test roles. Fill each cell with allowed or denied from real tests. Gaps in the matrix are your findings, and the matrix itself becomes strong report evidence.
For developers, move authorization out of individual resolver habits and into a shared business logic layer.
-
Check access for the specific object and field on every resolver, including nested and relationship fields.
-
Derive the caller identity from the authenticated session and pass it into the data layer, never from query arguments alone.
-
Apply field level rules for sensitive fields such as email, tokens, payment details and private membership.
-
Deny by default when a resolver has no explicit rule, and add two account tests for every new query, mutation and field.
-
Treat introspection settings as a hardening choice, not as the authorization fix. A hidden schema with a missing check is still a missing check.
-
Log cross account access denials and review them, because repeated probing of object identifiers often signals active testing or attack.
Verify the fix by replaying the exact two account cases from the report. The owning account should still succeed. The second account should receive a refusal or an empty result on the direct query, on the nested field path and on the mutation.
Closing takeaways
GraphQL does not remove authorization work. It multiplies the places where that work must happen, one resolver and one field at a time.
The disclosed pattern is consistent. REST checks, GraphQL forgets. Whenever an application offers both, compare them with two accounts.
Introspection is a map, not the treasure. The finding is private data or a private action reached without a check, proved with your own accounts and reported with minimum exposure.
Sources credited in this article: the public GraphQL security review linked in the body above, which cites HackerOne reports 489146, 885539 and 614355, and the public GraphQL guidance on authorization in the business logic layer. All research is based on publicly disclosed material only.
Tags: Bug Bounty, Cybersecurity, GraphQL, API Security, HackerOne
Written by Muhammad Asim Shahzad, SVP Head Application Security and VAPT at UBL, with thirteen plus years in offensive security, HackerOne Top 100 history, Synack Red Team experience and CVE 2021 31521 to his name. More writing and services at protector47.com. Connect with Asim on LinkedIn at https://www.linkedin.com/in/protector47 and find his public researcher history through HackerOne under the name protector47.