Introspection queries, depth limiting, cost analysis, field-level authorization, persisted queries, batching attacks, and the unique attack surface GraphQL exposes compared to REST. Bypassing disabled introspection via schema guessing.
Advanced16 min
GraphQLAPIIntrospection
Loading lesson…
GraphQL APIs present a unique security challenge: a single query can traverse deeply nested relationships, request fields with different sensitivity levels, and bypass traditional REST-centric rate limits. Standard web security tooling is often blind to GraphQL-specific attacks like introspection leaks, cost-based denial of service, and batching abuse. This lesson covers how attackers exploit these gaps and how to harden a GraphQL API without breaking its flexibility.
What you'll be able to do
Explain how GraphQL introspection leaks the entire schema to unauthenticated clients.
Describe depth limiting and cost analysis as query-level defences against resource exhaustion.
Understand why REST endpoint-level authorisation does not map to GraphQL field resolution.
Implement field-level authorisation checks in resolvers.
Recognise batching attacks that abuse list-type query fields to bypass rate limits.
Key terms
Introspection
GraphQL's built-in schema discovery query. Running query { __schema { types { name } } } returns every type, field, argument, and relationship defined in the schema. In production it should be disabled.
Depth limiting
Rejecting queries whose nesting depth exceeds a configured maximum. Prevents recursive or deeply nested queries like friends { friends { friends {...} } } from exhausting server resources.
Cost analysis
Assigning a numeric cost to each field and rejecting queries whose total cost exceeds a budget. A single users { friends { posts { ... } } } query might cost 50 points while the per-query budget is 30.
Persisted queries
A whitelist of pre-approved queries stored on the server. Clients send a hash instead of raw GraphQL; the server rejects any query not in the registry. Ad-hoc queries are blocked in production.
Batching attack
Querying multiple objects in a single request by passing an array to a list argument — users(id: [1,2,3]) — to bypass per-request rate limits while fetching data on dozens of records.
Resolver
A function responsible for fetching the data for a single GraphQL field. Each field in a query triggers its own resolver, which must independently check authorisation before returning data.
What is it?
GraphQL security attack surface
REST APIs have a well-understood security perimeter: each endpoint represents a single resource, and authorisation is checked at the controller level. GraphQL turns this model upside down. A singlePOST /graphql endpoint accepts arbitrary queries that can traverse the entire data graph. The attack surface is no longer a finite list of routes but the entire schema.
The most basic reconnaissance technique is introspection — a built-in GraphQL feature that returns the complete schema definition. An attacker sends query { __schema { types { name } } } and receives every type, field, argument, and relationship the API exposes, including internal fields like passwordHash or isAdmin that were never meant to be queried.
graphqlvulnerable
# Attacker discovers the entire schema in one query
query {
__schema {
types {
name
fields {
name
type {
name
}}}}}
# Response leaks internal types:
# User: id, name, email, passwordHash, isAdmin, internalNotes
# AdminPanel: secretUrl, resetAllPasswords, deleteUser
Disabling introspection in production is the first defence, but determined attackers brute-force field names using common patterns (id, name, email, users, admin) and public GraphQL schema registries. Introspection is merely a convenience — its absence does not prevent targeted queries.
Beyond information gathering, GraphQL introduces query-level resource exhaustion. A nested query like posts { comments { author { posts { } } } } triggers a resolver for each field, potentially causing exponential database load. Without depth limiting or cost analysis, a single cheap request can cascade into hundreds of database queries.
GraphQL query execution flow
Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.
Authorisation in GraphQL is fundamentally different from REST. In REST, you check permissions once at the controller level. In GraphQL, a single query hits multiple resolvers, each responsible for a different field. A user might have access to name and email but not passwordHash. Every resolver must independently verify that the requesting user is authorised for that specific field.
Persisted queries offer a stronger defence: instead of sending raw GraphQL, clients send a hash that maps to a pre-registered query on the server. Any ad-hoc query is rejected outright. This prevents attackers from crafting arbitrary queries even if they know the schema.
Batching attacks exploit GraphQL list arguments to query multiple records in a single HTTP request: users(id: [1, 2, 3, 4, 5]) { name email }. A per-request rate limiter sees one request while the server processes five database queries. This technique bypasses naive rate limits and is commonly used for data scraping.
Try it
GraphQL Security Playground
Explore how introspection, depth limiting, and field-level authorisation work in practice. Toggle introspection on and off, try batching attacks, and observe how each layer of defence blocks or permits the query.
GraphQL Playgroundprod
Ssecurity
graphql-api.truesapiens.io/graphql
POST
Query editor
Response
Run a query to see the response
Access loglast 0 requests
No requests yet. Click a preset or write a query to begin.
Real-world relevance
Facebook 2018 — the View As bug
One of the most damaging GraphQL security incidents in history was the Facebook “View As” bug disclosed in September 2018. Facebook had migrated its mobile API to GraphQL, and the new schema included a ViewAs field that returned the profile as seen by another user. This field was intended for the privacy check feature — letting users verify what their profile looks like to a specific friend.
The critical flaw: the ViewAs resolver did not verify that the requesting user owned the profile being viewed. An attacker could craft a GraphQL query that called ViewAs on any user ID, passing a friend token to bypass the privacy restrictions. Since the resolver only checked whether the viewing user had a friendship relationship — not whether they were the profile owner — it returned private data for any account.
graphqlvulnerable
# Attacker's crafted query — Facebook 2018
query stealProfile($targetUserId: ID!, $asUser: ID!) {
profile(userId: $targetUserId) {
name
email
birthday
posts {
content
visibility
}
viewAs(user: $asUser) {
# Returns profile as seen by $asUser —
# but never checks if requestor owns the profile
privateNotes
hiddenPhotos
friendList
}}}
The breach exposed personal data — including name, email, phone number, birthday, and recent searches — for approximately 30 million Facebook accounts. Attackers used the access tokens they had stolen through a separate credential stuffing attack to authenticate these GraphQL queries, but the root cause was the missing authorisation check in the ViewAs resolver. This incident is a textbook case of how field-level authorisation failures scale in GraphQL: one resolver in a schema of hundreds compromised the entire platform.
Facebook later added an ownership check to the ViewAs resolver and introduced automated security testing that validates every resolver has an authorisation guard. The incident led to wider industry adoption of cost analysis and resolver-level authorisation frameworks for GraphQL APIs.
Mitigation
Hardening a GraphQL API
Securing a GraphQL API requires defence at three layers: the transport layer, the query layer, and the resolver layer. At the transport layer, disable introspection in production, enforce authentication on the endpoint, and apply standard rate limiting per IP and per token.
At the query layer, implement depth limiting to reject queries beyond a configured nesting depth (typically 4–6 levels), and cost analysis to reject queries whose estimated computational cost exceeds a budget. Persisted queries provide the strongest protection by whitelisting allowed queries at build time.
At the resolver layer, every field resolver that accesses the database must check authorisation independently. Use a resolver wrapper or middleware pattern — similar to a REST controller guard — that inspects the field name, the requesting user's role, and the resource owner before returning data.
GraphQL introspection leaks the entire schema — disable it in production, but do not rely on it as the sole defence since attackers can brute-force field names.
Depth limiting prevents recursive query exhaustion; cost analysis assigns a computational budget per query to reject expensive traversals.
Each field resolver must independently check authorisation — REST-style endpoint-level guards do not protect against multi-field GraphQL queries.
Persisted queries create a whitelist of approved queries; ad-hoc queries are rejected at the transport layer.
Batching attacks use list arguments to query multiple records in one request — rate limiting must account for batch size, not just request count.
The Facebook 2018 View As bug (30 million accounts) was a single resolver missing an ownership check — a textbook field-level authorisation failure.