GraphQL Security Review: Depth Limits, Complexity Limits, Introspection, Abuse Cases
AI generated
{ }
type
GraphQL · Security · Depth Limits · Complexity · Introspection · Magento
GraphQL Security Review:
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.

22 min read Depth Limit · Complexity · Introspection · Batching · Rate Limiting GraphQL Armor · Magento · OWASP GraphQL

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.

11. FAQ: GraphQL Security Review, Depth, Complexity, Introspection, Abuse Cases

1Depth limit: which value for Magento?
7 to 10 levels are realistic for Magento, since types are inherently deep. Start with 10, analyze real queries, and lower it if needed. The depth limit is checked before execution, so there is no performance overhead.
2Why is introspection dangerous in production?
It hands attackers the complete API map. Used deliberately for follow-up attacks against sensitive fields and mutations. Disable it in production or restrict it to authorized clients.
3What is a batching attack?
100 login mutations in one HTTP request equals 100 brute-force attempts with no effect from HTTP rate limiting. Protection: cap batching at max 5 to 10 operations and implement operation-level rate limiting.
4What is alias abuse?
Calling the same expensive resolver 50 times via aliases, bypassing naive complexity limits. An alias limit (maxAliases: 5) prevents this. GraphQL Armor implements it as a drop-in plugin.
5Persisted queries: when are they worthwhile?
For frontends with a fixed query set (Hyva, React). Eliminates all dynamic query attack vectors. Not suitable for public APIs with dynamic queries.
6How to disable introspection in Magento?
Nginx: reject requests with __schema or __type in the body with a 403. Or as a GraphQL validator plugin before execution. Keep developer access via IP allowlisting.
7How to calibrate complexity limits?
Scalars = 1, objects = 1 + sum of fields, lists = pageSize times element cost. Analyze real production queries. Limit = 2x the maximum legitimate query complexity as a starting point.
8Is GraphQL Armor suitable for Magento?
GraphQL Armor is JavaScript-first (Apollo, Yoga). For Magento (PHP): implement your own middleware or webserver-level rules for the most important measures.
9Is rate limiting alone enough?
No. Batching bypasses HTTP-level rate limiting. Defense in depth: combine rate limiting with depth limits, complexity limits, alias limits, and ideally persisted queries.
10How to test a GraphQL API for vulnerabilities?
Tools: graphql-cop, clairvoyance. Manually: test introspection, deep queries, batching with 100 ops, 50 aliases. Every successful attack reveals a missing protective measure.