Depth, Complexity, Introspection, and Abuse Cases
Without additional protective measures, GraphQL APIs are vulnerable to overloaded queries, introspection attacks, batching DoS, and data leaks through resolvers that are too permissive. This security review shows concrete attack scenarios and the matching countermeasures.
Table of Contents
- 1. The distinctive attack surface of GraphQL APIs
- 2. Query depth limiting: blocking deep nesting
- 3. Complexity limits: correctly scoring expensive queries
- 4. Introspection: when to disable it and how to protect it
- 5. Batching attacks and alias abuse
- 6. Rate limiting and query allowlisting
- 7. Magento GraphQL security configuration
- 8. Security measures compared
- 9. Summary
- 10. The key takeaways at a glance
- 11. FAQ
1. The distinctive attack surface of GraphQL APIs
GraphQL APIs have a markedly different attack surface than REST APIs. While REST endpoints return fixed, well-defined amounts of data, GraphQL gives clients the power to formulate arbitrarily deep and broad queries. Without protective measures, a single HTTP request can send a query that traverses millions of records, extracts every available piece of schema information, or executes the same expensive resolver hundreds of times within a single request. These are not theoretical threats: security researchers and attackers exploit these characteristics systematically.
The OWASP GraphQL Cheat Sheet lists several specific attack classes: overloaded queries (extremely deep or broad queries aimed at resource exhaustion), introspection attacks (schema enumeration for more targeted follow-up attacks), batching attacks (many operations in one request to enable brute force without triggering rate limiting), field stuffing (too many fields in a single request), and resolver-based data leaks (fields that return sensitive data without an authorization check). Each of these attack classes requires its own protective measure: no single control defends against all of them.
2. Query depth limiting: blocking deep nesting
Query depth limiting is the simplest and most direct defense against overloaded-query attacks. GraphQL queries can be nested arbitrarily deep: if the schema allows products to reference related products, and those to reference their own related products, a potentially infinite chain emerges. A query that traverses this chain even just 10 levels deep can trigger millions of database accesses.
Depth limiting counts the maximum nesting depth of a query before execution and rejects queries that exceed a defined threshold. A typical value for commerce APIs lies between 5 and 10 levels. For Magento GraphQL, which has inherently deep types (product to price to minimum price to final price to amount), 7 to 8 levels is a realistic baseline. GraphQL Armor implements depth limiting as a plugin for Apollo Server; for PHP-based GraphQL servers like Magento, equivalent middleware implementations or webserver-level regex filters serve as a first line of defense.
# DANGEROUS: query with extreme depth (depth = 8+)
# Triggers exponentially many database accesses
# A depth limit blocks this query before execution
query DeepAttack {
products(search: "shoe") { # Depth 1
items { # Depth 2
related_products { # Depth 3
sku
related_products { # Depth 4
sku
related_products { # Depth 5
sku
related_products { # Depth 6
sku
related_products { # Depth 7
sku
}
}
}
}
}
}
}
}
# SAFE: flat query with normal information needs
# Depth = 5, within the typical limit of 7
query SafeProductQuery {
products(search: "shoe") { # Depth 1
items { # Depth 2
sku
name
price_range { # Depth 3
minimum_price { # Depth 4
final_price { value currency } # Depth 5
}
}
}
}
}
3. Complexity limits: correctly scoring expensive queries
Complexity limits are more powerful than depth limits because they score the cost of a query instead of merely measuring its depth. A broad but flat query, many fields at the same nesting level, is not blocked by a pure depth limit but can still be expensive. Complexity calculation assigns a cost value to every field and sums these costs across the entire query. List fields typically receive higher costs because each element in the list triggers additional resolver calls. Simple scalar fields receive low costs.
The challenge in complexity design lies in calibrating field costs. Costs that are too low for list fields fail to provide meaningful protection. Costs that are too high for common fields make normal queries unreachable. A proven approach: simple scalars cost 1, objects cost 1 plus the sum of their fields, and lists cost the multiplier of their maximum length times the cost of one element. For Magento queries this means: a product list with pageSize: 100 multiplies the cost of all product fields by 100. A limit of 1000 would still allow this query with 100 products at 5 fields each (= 500), but would block it with 200 products.
4. Introspection: when to disable it and how to protect it
GraphQL introspection is the powerful feature that makes tools like GraphiQL and Altair possible: clients can query the entire schema, including every type, field, argument, and directive. In development environments this is indispensable. In production environments it is a double-edged sword: introspection hands attackers a complete map of the API. With a single __schema query, an attacker learns every field, every type, and every available operation, which makes targeted follow-up attacks significantly easier.
The recommended strategy for production introspection: disable it entirely for public clients, and grant restricted access to authorized developer tools via IP allowlisting or API keys. Alternatively, there is the "schema filtering" approach: introspection stays active, but sensitive fields (internal IDs, debug fields, admin mutations) are filtered out of the introspection result. For Magento this means: introspection is enabled by default but can be disabled for production traffic via an HTTP header check or middleware. GraphQL Armor offers a simple configuration option for this.
# ATTACK: full schema enumeration via introspection
# Hands attackers a complete API map
query IntrospectionAttack {
__schema {
queryType { name }
mutationType { name }
types {
name
kind
fields {
name
type { name kind ofType { name kind } }
args { name type { name kind } }
}
}
}
}
# Block in production: an introspection handler checks
# whether the query contains __schema or __type and rejects it.
# ALLOWED in development/staging:
# Introspection for authorized clients with an API key
# ALLOWED everywhere: single-type introspection for __typename
# (needed for Apollo Client's type policies)
query TypenameOnly {
products(search: "test") {
items { __typename }
}
}
5. Batching attacks and alias abuse
GraphQL batching lets multiple operations be bundled into a single HTTP request. That is a legitimate performance feature, but it gets abused for attacks. A batching attack, for example, sends 100 login mutations in a single HTTP request to bypass rate limiting that is measured at the HTTP request level: one HTTP request contains 100 login attempts instead of 100 separate HTTP requests. Without batching protection, classic brute-force rate limiting becomes ineffective.
A related attack form is alias abuse: GraphQL allows aliases, different names for the same field within one query. A query can call the same expensive resolver 50 times with different aliases and thereby bypass complexity limits that count only once per field name. Countermeasures: cap batching at a maximum of 5 to 10 operations per request, implement alias limiting as a separate validator, and run complexity calculation so that aliases are fully counted. GraphQL Armor implements alias limiting and batching limits as separate plugins.
6. Rate limiting and query allowlisting
Rate limiting for GraphQL APIs must happen at the operation level, not just the HTTP request level. Because GraphQL batching can bundle multiple operations into one request, an HTTP-level rate limit is insufficient. Effective GraphQL rate limiting combines: HTTP request rate per client IP or API key, operation rate per named operation, and query complexity as a cumulative limit over a time period. A client might, for example, be allowed to consume 1000 complexity points per minute; a single expensive query burns through that quickly, while many cheap queries can be spread out.
Persisted queries or query allowlisting are the strongest protective measure for production APIs: only predefined, approved queries are accepted. Dynamic query strings from unknown clients are rejected outright. This eliminates the entire query injection attack vector and makes depth limits and complexity limits redundant for known clients, though they remain in place as protection against clients that still send invalid queries. For Magento headless frontends with a fixed query set, persisted query allowlisting is straightforward to implement; for public APIs that must allow dynamic queries, complexity-based rate limiting remains the primary defense.
# ALIAS ABUSE: 50 expensive resolver calls in one query
# Bypasses naive complexity limits that count only once per field name
query AliasAttack {
p1: products(search: "shoe", pageSize: 100) { total_count items { sku price_range { minimum_price { final_price { value } } } } }
p2: products(search: "shirt", pageSize: 100) { total_count items { sku price_range { minimum_price { final_price { value } } } } }
p3: products(search: "bag", pageSize: 100) { total_count items { sku price_range { minimum_price { final_price { value } } } } }
# ... 47 more aliases
}
# PROTECTION: alias limit of maximum 5 per query
# Every alias counts fully toward complexity
# GraphQL Armor: maxAliases: 5
# BATCHING ATTACK: 100 login attempts in one HTTP request
# Fully bypasses HTTP-level rate limiting
# [{ "query": "mutation { generateCustomerToken(email: \"test@x.de\", password: \"pass1\") { token } }" },
# { "query": "mutation { generateCustomerToken(email: \"test@x.de\", password: \"pass2\") { token } }" },
# ... 98 more attempts ]
# PROTECTION: cap batching at max 5 operations
# Rate limiting at the operation level, not just the HTTP level
mutation LoginAttempt($email: String!, $password: String!) {
generateCustomerToken(email: $email, password: $password) {
token
}
}
7. Magento GraphQL security configuration
Magento 2 ships with a few built-in GraphQL security mechanisms. However, the default values are not always optimized for every production scenario. The most important security feature: Magento checks the bearer token on authenticated queries and returns an authorization error for an invalid or expired token. Customer data such as orders, addresses, and wishlists cannot be retrieved without a valid token.
For additional Magento GraphQL security, the following measures are recommended: disable introspection via Nginx or Apache configuration for production traffic (reject any request whose body contains __schema or __type with a 403). Depth limiting at the webserver level through request body analysis, or as a Magento plugin ahead of the resolver stack. Rate limiting for the /graphql route via Nginx limit_req_zone as a first layer. For higher security requirements: enable WAF rules (Cloudflare, AWS WAF) with GraphQL-aware analysis rules. Implement persisted queries for the Hyva frontend via Apollo persisted query links, so that only known query hashes are allowed.
8. Security measures compared
Every GraphQL security measure protects against specific attack classes. A comprehensive security strategy combines several layers.
| Measure | Protects against | Implementation | Effort |
|---|---|---|---|
| Depth limit | Deep nesting attacks, recursive queries | GraphQL Armor, validation rule | Low |
| Complexity limit | Broad, expensive queries, list abuse | GraphQL Armor, custom validator | Medium (calibration needed) |
| Introspection disabled | Schema enumeration, targeted follow-up attacks | Nginx rule, GraphQL config | Low |
| Alias limit | Alias abuse, complexity limit bypass | GraphQL Armor (maxAliases) | Low |
| Batching limit | Batching DoS, brute force via batching | Middleware, GraphQL Armor | Low |
| Persisted queries | All dynamic query attacks, completely | Apollo persisted queries, CDN | High (only for fixed frontends) |
The most important insight for Magento GraphQL security: no single measure is sufficient. An attacker who gets past the depth limit fails at the complexity limit. Anyone who overcomes both is slowed down by rate limiting. Disabling introspection makes it harder to find targets in the first place. Persisted queries for the Hyva frontend close off the attack vector for the frontend entirely, while the admin API still offers developers full GraphQL flexibility.
9. Summary
GraphQL APIs have a different security profile than REST APIs: the flexibility that makes GraphQL powerful is at the same time the source of its largest attack surfaces. Depth limits block deep nesting attacks. Complexity limits score queries by their actual cost and block broad, expensive queries. Introspection should be disabled in production or restricted to authorized clients. Alias limits and batching limits close attack vectors that naive protective measures miss. Persisted queries offer the strongest protection for frontends with a fixed query set.
For Magento GraphQL APIs, a defense-in-depth strategy is recommended: depth and complexity limits as a baseline, introspection disabled via Nginx for public traffic, rate limiting at the webserver level as a first layer, and persisted queries for the Hyva frontend as the strongest measure. Regular security reviews that track new OWASP GraphQL recommendations round out the security strategy.
GraphQL Security Review: the key takeaways at a glance
Depth + Complexity
Depth limit for nesting depth (7 to 10 for Magento), complexity limit for total cost: together they block the most common DoS attack vectors.
Introspection
Disable for public clients in production, via an Nginx rule or GraphQL config. Keep developer access via IP allowlisting or API key.
Alias and batching limits
Alias abuse and batching DoS are attack vectors that bypass depth and complexity limits. GraphQL Armor implements maxAliases and batching limits as drop-in plugins.
Persisted queries
The strongest measure for fixed frontends: allow only known query hashes. Fully eliminates dynamic query attack vectors for the Hyva frontend.