GraphQL-Specific Attacks: Introspection, Query Depth, Batching
AI generated
OWASP
0x00
Security · GraphQL · API Security · Magento 2
GraphQL-Specific Attacks: Introspection, Query Depth, Batching
How attackers exploit Magento GraphQL endpoints, and how you harden them

GraphQL endpoints in Magento stores open new attack paths: enabled introspection exposes the full schema, deeply nested queries overload the database and server, and alias-based batching enables brute-force attacks against login forms. This article shows how to disable introspection in production, limit query depth and complexity, control batching, and correctly implement field-level authorization so your Magento GraphQL endpoint stays resilient against targeted attacks.

16 min. read Introspection · Query Depth · Batching Magento 2.4.8 · GraphQL · webonyx/graphql-php

1. Why GraphQL needs different security thinking than REST

REST APIs consist of many individual endpoints, each mapping to a clearly scoped resource access - rate limiting, WAF rules, and authorization middleware can be configured granularly per route. GraphQL flips this model: a single POST endpoint, usually /graphql, accepts arbitrarily shaped queries that the client assembles itself. This flexibility is GraphQL's big advantage for frontend teams, but it is also the central attack surface, because classic URL-based security tools are blind to it.

Securing a GraphQL API the same way you would a REST API misses the actual risks: an attacker can use the selection set structure itself to decide how deeply nested, how widely fanned out, and how many times the same resource gets queried within a single request. Defenses therefore have to target the query structure itself: introspection control, depth and complexity limits, batching rules, and field-accurate authorization - topics this article works through systematically, with direct ties to Magento's GraphQL implementation.

2. Introspection queries: when the full schema is exposed

GraphQL ships with built-in self-documentation via the __schema introspection query: a single request returns every type, field, argument, mutation, and even fields marked deprecated but still active. Tools like GraphQL Voyager or the InQL extension for Burp Suite automatically turn that response into a complete, interactive map of the API, with no documentation or source code access required. What is a convenience feature in development environments becomes a reconnaissance tool for attackers in production.

Especially critical: introspection reveals not just publicly used fields, but also internal, administrative, or experimental mutations that the frontend never calls but that are technically reachable through the endpoint. An attacker can identify potential targets like customer-data resolvers or price-change mutations within seconds, without ever looking at the source code. Disabling introspection in production is therefore not an optional hardening step but one of the single most effective measures available.

3. Query depth and nested queries: the resource exhaustion attack

GraphQL schemas frequently contain cyclical relationships: a category references products, products reference related products, related products reference categories again. Without a technical limit, a client can nest these cycles arbitrarily deep and trigger exponentially many resolver calls on the server with a single, syntactically valid query. Each additional level of nesting multiplies the number of database queries, so a single query with ten to fifteen levels of depth is already enough to bring the database and PHP workers to their knees.

The example below is shown purely for illustration, so it is clear exactly what depth limits are meant to stop. In practice, such a request would be rejected with a validation error by server-side depth limiting before execution, rather than being allowed to start firing resolver calls in the first place.


# Illustrative example of a maliciously nested query used to
# exhaust server resources through recursive schema expansion.
# This pattern should be rejected by depth/complexity limits
# before any resolver executes.
query MaliciousNestedQuery {
  categoryList {
    children {
      children {
        children {
          children {
            children {
              products(pageSize: 100) {
                items {
                  sku
                  related_products {
                    sku
                    related_products {
                      sku
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

4. Query complexity and cost analysis as a defense mechanism

A pure depth limit is not enough, because shallow queries can be expensive too: a query with hundreds of aliases on the same expensive field, combined with large pageSize values on connections, generates enormous server load without ever exceeding the nesting depth. Query complexity analysis solves this by assigning a cost value to every field, multiplying list fields by their multiplier (such as the requested pageSize), and checking the sum of all costs against an overall limit before the resolver even runs.

The PHP reference implementation webonyx/graphql-php, which Magento's GraphQL module is also built on, ships two ready-made validation rules, QueryComplexity and QueryDepth, that can be registered as DocumentValidator rules. It is important to derive realistic thresholds from actual frontend queries rather than picking arbitrary numbers: limits set too low block legitimate storefront queries, while limits set too high offer no real protection against resource exhaustion.


<?php
declare(strict_types=1);

namespace Mironsoft\GraphQlSecurity\Model;

use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\Rules\QueryDepth;
use GraphQL\Validator\Rules\DisableIntrospection;
use GraphQL\Validator\DocumentValidator;

/**
 * Registers hard limits for GraphQL query depth and complexity
 * to prevent resource exhaustion through nested or wide queries.
 */
class QueryValidationRules
{
    private const MAX_QUERY_DEPTH = 8;
    private const MAX_QUERY_COMPLEXITY = 300;

    /**
     * Adds depth, complexity and introspection rules to the validator.
     *
     * @param bool $isProduction Whether the current environment is production
     * @return void
     */
    public function register(bool $isProduction): void
    {
        DocumentValidator::addRule(new QueryDepth(self::MAX_QUERY_DEPTH));
        DocumentValidator::addRule(new QueryComplexity(self::MAX_QUERY_COMPLEXITY));

        if ($isProduction) {
            // Block __schema and __type introspection queries in production
            DocumentValidator::addRule(new DisableIntrospection());
        }
    }
}

5. Batching abuse: brute force and credential stuffing via aliases

GraphQL allows bundling multiple operations under different alias names into a single HTTP request. What is intended as a performance feature, letting several data requests happen in one round trip, becomes a gateway for brute-force attacks: an attacker bundles hundreds of aliased generateCustomerToken mutations with different passwords for the same email address into a single request. IP-based rate limiters that count HTTP requests instead of individual GraphQL operations register only one single request and remain ineffective.

The same pattern works for credential stuffing against customer accounts using leaked credentials from other services, for enumerating valid email addresses via registration mutations, and for guessing coupon or discount codes. Effective protection requires rate limiting that counts at the operation level rather than the HTTP request level, combined with a hard cap on the number of allowed operations per request.


# Illustrative example of alias-based batching abuse. A single
# HTTP request bundles many login attempts as aliased operations,
# bypassing rate limiters that count requests instead of operations.
# Shown purely to explain what operation-aware rate limiting must block.
mutation CredentialStuffingAttempt {
  attempt1: generateCustomerToken(email: "victim@example.com", password: "password1") { token }
  attempt2: generateCustomerToken(email: "victim@example.com", password: "password2") { token }
  attempt3: generateCustomerToken(email: "victim@example.com", password: "123456") { token }
  attempt4: generateCustomerToken(email: "victim@example.com", password: "qwerty") { token }
  # ... hundreds of further aliased attempts within the same request
}

6. Field-level authorization gaps despite endpoint auth

A bearer token or a valid session at the /graphql endpoint only confirms that a request is authenticated in general - it says nothing about whether the requesting user is authorized to execute a specific field or mutation. Many GraphQL API vulnerabilities arise exactly in this gap: a custom resolver checks that a logged-in customer is making the request at all, but forgets to check whether the requested order or customer ID actually belongs to that customer.

The result is a classic IDOR vulnerability (Insecure Direct Object Reference), just expressed through GraphQL field arguments instead of REST URL parameters. Every resolver that accesses an ID, an email address, or any other direct reference must independently validate against the current context (customer ID, store view, ACL role), regardless of whether the parent query type is already authenticated. Authorization belongs in the resolver, not just at the endpoint.

7. Rate limiting and persisted queries as a defense layer

Persisted queries flip the trust model: instead of accepting arbitrary query strings from the client, the server pre-stores an allowlist of known, vetted queries under a hash. In production, the client sends only the hash instead of the full query text, the server executes only queries from that allowlist, and rejects everything else. This eliminates the entire attack surface from arbitrarily constructed, deeply nested, or alias-heavy queries, because new query shapes simply cannot be executed.

On top of that, rate limiting should be operation-aware rather than request-aware: instead of counting HTTP requests, an effective limiter counts individual GraphQL operations within a request and applies different thresholds depending on the operation - a generateCustomerToken mutation should tolerate far fewer attempts per minute than a read-only product search. Persisted queries and operation-aware rate limiting combine into a layered defense.


{
  "persisted_queries": {
    "a3f1c9e2b7d4f6a8": "query ProductDetail($sku: String!) { products(filter: {sku: {eq: $sku}}) { items { sku name price_range { minimum_price { final_price { value } } } } } }",
    "9b7e2f4a1c0dbe33": "mutation Login($email: String!, $password: String!) { generateCustomerToken(email: $email, password: $password) { token } }"
  },
  "rate_limits": {
    "generateCustomerToken": { "max_operations_per_minute": 5, "scope": "ip+email" },
    "default": { "max_operations_per_minute": 120, "scope": "ip" }
  }
}

8. Magento-specific GraphQL configuration and ACLs

Magento's GraphQL module is built on webonyx/graphql-php and can be hardened through custom plugins. A plugin on the central query processor lets you register additional validation rules such as QueryDepth and QueryComplexity without touching the core, in line with the plugin-over-preference principle. Since Magento 2.4.4, basic configuration options for query depth and complexity have also existed via the graphql node in app/etc/env.php, which can be set differently per environment: generous in development, restrictive in production.

The same principle applies to introspection: it stays enabled in development and staging environments so frontend teams and tools like Postman or Apollo Studio can load the schema, while it gets disabled in production through a dedicated DisableIntrospection validation rule. In addition, custom GraphQL resolvers that access admin functionality should define their own permission resources via acl.xml and explicitly check them against the current admin context inside the resolver, rather than relying on the resolver's mere reachability.


<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <!-- Inject custom depth/complexity validation rules into Magento's GraphQL query processor -->
    <type name="Magento\Framework\GraphQl\Query\QueryProcessor">
        <plugin name="mironsoft_graphql_query_validation"
                type="Mironsoft\GraphQlSecurity\Plugin\RegisterValidationRulesPlugin"
                sortOrder="10"/>
    </type>

    <!-- Default values, override per environment via app/etc/env.php -->
    <type name="Mironsoft\GraphQlSecurity\Model\QueryValidationRules">
        <arguments>
            <argument name="maxQueryDepth" xsi:type="number">8</argument>
            <argument name="maxQueryComplexity" xsi:type="number">300</argument>
        </arguments>
    </type>
</config>

9. Monitoring and logging GraphQL abuse

Because a single GraphQL request can be arbitrarily complex, classic access-log monitoring based on request counts is not enough. Effective monitoring logs the calculated query complexity, the actual nesting depth, and the number of operations contained in each request as structured fields, not just status code and response time. This makes anomalies visible, such as a sudden rise in average complexity scores or a cluster of requests sitting right at the maximum allowed depth, both of which hint at systematic probing of the limits.

Repeated __schema requests in production, an unusually high number of aliases on generateCustomerToken, or a noticeable spike in rejected validation errors are strong indicators of active reconnaissance or attack attempts and should feed into a SIEM or APM system such as New Relic or Elastic APM, complete with alerting thresholds. A dashboard that visualizes query complexity distribution, introspection attempts, and rejected validation errors over time surfaces attack patterns that get lost in plain HTTP status code logs.

Attack vector Secure state Typical mistake Recommended measure
Introspection Disabled in production __schema query returns the full schema DisableIntrospection rule for prod requests
Query depth Limited to a sensible maximum Unbounded nested queries QueryDepth rule + Magento query_depth
Query complexity Cost-based overall limit Wide alias queries left unlimited QueryComplexity rule with field weighting
Batching Limited operation count Hundreds of aliased mutations per request Operation count limit + persisted queries
Field authorization Checked per resolver Endpoint auth only, resolver trusts blindly ACL check and context validation per resolver

All five attack vectors reinforce each other: an attacker who uses introspection to map the schema then applies that knowledge specifically to deep queries, batching abuse, or hunting for unprotected resolvers. Securing only a single vector leaves the others exposed, effective protection only comes from combining all the measures from the table.

Mironsoft

GraphQL security audits and API hardening for Magento stores

Ready to harden your GraphQL endpoint?

We analyze your Magento GraphQL endpoint for introspection exposure, missing depth and complexity limits, and authorization gaps, and implement targeted hardening measures, from validation rules to persisted queries and monitoring.

GraphQL security audit

Introspection, depth, and batching analysis with a prioritized action plan

Query limits & authorization

Depth/complexity rules, resolver-level authorization, and ACL hardening

Monitoring setup

Complexity logging, alerting, and anomaly detection for GraphQL traffic

10. Summary

The GraphQL-specific attack vectors of introspection, query depth, query complexity, batching abuse, and field-level authorization gaps differ fundamentally from classic REST vulnerabilities, because they target the query structure itself rather than individual URLs. Disabling introspection in production, enforcing hard depth and complexity limits, and capping batching at a sensible level close the three largest entry points with a manageable implementation effort.

Beyond that, authorization needs to live consistently in every single resolver rather than just at the endpoint, and persisted queries structurally reduce the attack surface by ruling out arbitrary query construction from the start. Combining these measures with operation-aware rate limiting and structured monitoring makes your Magento GraphQL endpoint robust against the most common attack patterns without sacrificing the flexibility legitimate frontend applications rely on.

GraphQL Security for Magento Stores - The Essentials at a Glance

Disable introspection

Block it in production via a DisableIntrospection rule, keep it enabled in dev/staging.

Limit depth & complexity

Derive QueryDepth and QueryComplexity thresholds from real frontend queries.

Control batching

Cap the operation count per request, rate-limit at the operation level, not the request level.

Enforce field authorization

Every resolver checks permissions independently against customer/admin context, not just the endpoint.

11. FAQ: GraphQL-Specific Attacks on Magento

1What is a GraphQL introspection query and why is it risky in production?
A built-in GraphQL feature that returns the full API schema with types, fields, and mutations via the __schema query. In production it lets attackers automatically map the API surface.
2How does query depth differ from query complexity?
Query depth only measures nesting levels. Query complexity additionally evaluates field costs including list multipliers like pageSize, and also catches shallow but wide and expensive queries.
3What is a typical example of a resource exhaustion attack via GraphQL?
A query that repeatedly nests cyclical schema relationships like category, product, related products triggers exponentially more resolver calls per level and can overload the server and database.
4How does batching abuse work for brute-force attacks?
Attackers use GraphQL aliases to bundle hundreds of login attempts into a single HTTP request. IP-based rate limiters that count requests instead of operations register only one request and fail to catch it.
5Is endpoint-level authentication enough for GraphQL?
No. A valid token only confirms authentication, not authorization for a specific field or mutation. Every resolver must independently check permissions.
6What are persisted queries and how do they help against attacks?
A server-side allowlist of vetted query strings under a hash. The client sends only the hash, so arbitrarily constructed attack queries cannot be executed at all.
7How do I configure query depth limits in Magento?
Through a custom plugin on the GraphQL query processor with QueryDepth/QueryComplexity rules from webonyx/graphql-php. Since Magento 2.4.4 also via the graphql node in app/etc/env.php.
8Which Magento version supports native GraphQL query limits?
Basic options exist since Magento 2.4.4. For fine-grained control and disabling introspection in production, a custom plugin is additionally recommended.
9How do I detect GraphQL abuse in monitoring?
Structured logging of query complexity, nesting depth, and operation count surfaces anomalies. Repeated __schema requests and clusters of validation errors are strong indicators.
10Should I disable GraphQL entirely if I prefer REST?
Not necessarily. With introspection blocking, depth/complexity limits, batching control, and resolver-level authorization, GraphQL can be hardened just as robustly as a REST API.