GraphQL Query Complexity Limiting in Magento: Schutz vor überladenen Queries
AI generated
M2
di.xml
Magento 2 · GraphQL · Security
GraphQL Query Complexity Limiting
wie Magento überladene Queries erkennt und wie eigene Limits das ergänzen

Eine tief verschachtelte oder stark aliasierte GraphQL-Query kann in einem einzigen HTTP-Request ein Vielfaches der Datenbankarbeit auslösen, die eine vergleichbare REST-Anfrage verursachen würde. Dieser Artikel zeigt, wie Magentos eingebaute queryDepth- und queryComplexity-Limits funktionieren, was die seit Version 2.4.9 verfügbaren Alias- und Query-Length-Limits im Admin zusätzlich abdecken, und wie sich für besonders teure Felder eine eigene, feinere Komplexitätsberechnung ergänzen lässt.

14 Min. Lesezeit queryDepth · queryComplexity Alias- und Query-Length-Limits seit 2.4.9

1. Warum tief verschachtelte und breite Queries eine DoS-Gefahr sind

GraphQL erlaubt es einem Client, in einer einzigen Anfrage auszudrücken, wofür eine REST-Schnittstelle Dutzende einzelner Requests bräuchte. Genau das macht GraphQL für legitime Clients so attraktiv, öffnet aber auch die Tür für Anfragen, die serverseitig ein Vielfaches der Arbeit auslösen, die die Größe des Requests vermuten lässt. Anders als bei REST, wo jeder Endpunkt einen ungefähr begrenzten Aufwand hat, kann eine einzelne GraphQL-Query nahezu beliebig viele Datenbankoperationen anstoßen.

Ein konkretes Beispiel: eine Query, die den Kategoriebaum über mehrere Ebenen mit children { children { children { ... } } } abfragt, oder dasselbe Feld zweihundertfach unter verschiedenen Aliasen anfordert. Beide Muster lassen sich mit vergleichsweise wenigen Zeichen im Query-Text formulieren, erzeugen serverseitig aber eine Datenbanklast, die in keinem Verhältnis zur Größe der Anfrage steht, genau das ist der klassische Angriffsvektor gegen ungesicherte GraphQL-Endpunkte.


# Beispiel für ein Muster, das ohne Limits eine unverhältnismäßige Last erzeugt
query AliasFlood {
  a1: products(search: "x") { items { sku } }
  a2: products(search: "x") { items { sku } }
  a3: products(search: "x") { items { sku } }
  # ... in der Praxis hunderte weitere Aliase derselben teuren Query
}

2. Wie ein GraphQL-Server Tiefe und Komplexität überhaupt berechnet

Magentos GraphQL-Schicht baut auf der PHP-Bibliothek webonyx/graphql-php auf, deren Validierungsregeln QueryDepth und QueryComplexity bereits vor der eigentlichen Ausführung gegen den geparsten Abstract Syntax Tree der Anfrage laufen. Eine zu tiefe oder zu komplexe Query wird also abgelehnt, bevor auch nur ein einziger Resolver aufgerufen wurde, das schützt die Datenbank effektiver als jede nachträgliche Drosselung.

QueryDepth zählt schlicht die Verschachtelungsebenen der angefragten Selektionen. QueryComplexity vergibt standardmäßig einen Punkt pro angefragtem Feld, verschachtelte Felder summieren sich in ihre Elternfelder hinein, Fragmente selbst zählen dabei nicht zusätzlich. Beide Regeln bewerten also ausschließlich die Form der Query, ohne zu wissen, was ein bestimmtes Feld beim tatsächlichen Auflösen wirklich kostet.

3. Magentos eingebaute Limits: queryDepth und queryComplexity

Magento verdrahtet diese beiden webonyx-Regeln über Magento\Framework\GraphQl\Query\QueryComplexityLimiter im Framework-Layer, mit den Standardwerten queryDepth gleich zwanzig und queryComplexity gleich dreihundert. Entscheidend ist: Beide Werte sind reine Konstruktor-Argumente, ausschließlich über di.xml konfigurierbar, für keinen der beiden Werte existiert eine Store-Configuration-Oberfläche im Admin.

Diese fehlende Admin-Oberfläche ist eine bewusste Entscheidung, kein Versehen: Die Form einer erlaubten Query ist eine architektonische Grundsatzfrage, keine Einstellung, die pro Store-View beliebig verändert werden sollte. Jede Anpassung erfordert entsprechend ein reguläres Deployment inklusive setup:di:compile.


<!-- app/code/Vendor/GraphQlHardening/etc/di.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Framework\GraphQl\Query\QueryComplexityLimiter">
        <arguments>
            <argument name="queryDepth" xsi:type="number">12</argument>
            <argument name="queryComplexity" xsi:type="number">150</argument>
        </arguments>
    </type>
</config>

4. Admin-UI-Limits seit 2.4.9: Alias- und Query-Length-Begrenzung

Mit Magento 2.4.9 kam eine zweite, tatsächlich im Admin konfigurierbare Schutzschicht hinzu, erreichbar unter Stores, Konfiguration, Services, Magento Web API, GraphQl Input Limits. Sie deckt zwei Vektoren ab, die reines Complexity-Limiting nicht vollständig schließt: die Anzahl erlaubter Aliase pro Query, standardmäßig zehn, und die maximale Zeichenlänge einer Query, standardmäßig etwas über ein Megabyte.

Dahinter stehen die Konfigurationspfade graphql/validation/alias_limit_enabled und graphql/validation/maximum_alias_allowed sowie graphql/validation/query_length_limit_enabled und graphql/validation/query_length_limit_allowed. Anders als Depth und Complexity lassen sich diese Werte ohne Deployment pro Store anpassen, praktisch etwa dann, wenn eine bestimmte Headless-Storefront tatsächlich größere, aber trotzdem legitime Queries benötigt.

5. Eigene Depth- und Complexity-Werte per di.xml anpassen

Ob eine Verschärfung oder Lockerung der Standardwerte sinnvoll ist, hängt vom konkreten Einsatzzweck ab. Ein öffentlicher, unauthentifizierter GraphQL-Endpunkt vor einem Katalog mit tiefen Kategoriebäumen profitiert von einem strengeren Limit als die Standardwerte, während ein internes, hinter Persisted Queries betriebenes PWA-Studio-Frontend mit bekanntem, begrenztem Query-Set durchaus höhere Werte vertragen kann.

Wichtig ist, eine Lockerung des Complexity-Limits immer mit Persisted Queries in Produktion zu kombinieren, damit die tatsächlich ausgeführten Query-Formen bekannt und dadurch faktisch begrenzt bleiben, statt einem offenen Endpunkt ein großzügiges Komplexitätsbudget zu geben, das jeder beliebige Client ausnutzen kann.


<!-- Beispiel für ein internes, hinter Persisted Queries betriebenes Frontend -->
<type name="Magento\Framework\GraphQl\Query\QueryComplexityLimiter">
    <arguments>
        <argument name="queryDepth" xsi:type="number">25</argument>
        <argument name="queryComplexity" xsi:type="number">450</argument>
    </arguments>
</type>

6. Grenzen des flachen 1-Punkt-Modells

Das Standardmodell gewichtet jedes Feld gleich: ein einfaches Skalarfeld wie sku kostet denselben einen Punkt wie ein berechnetes Feld, das eine vollständige Preiskalkulation, eine Facetten-Aggregation über den gesamten Katalog oder eine Volltextsuche auslöst. Eine Query, die anhand der Feldanzahl günstig aussieht, kann trotzdem die mit Abstand teuerste Query im gesamten Store sein.

Genau diese Diskrepanz ist der blinde Fleck reiner Depth- und Complexity-Limits: Sie begrenzen die Form einer Query, nicht die tatsächlichen Resolver-Kosten. Ein Angreifer kann komfortabel unter dreihundert Punkten bleiben und trotzdem wiederholt genau die teuersten Resolver über geschickt platzierte Aliase ansteuern.

7. Eigene Complexity-Berechnung für kritische Felder ergänzen

Magento bietet dafür weder eine @complexity-Direktive noch einen Cost-Callback pro Feld, wie ihn manche andere auf graphql-php basierende Server kennen. Wer teure Felder trotzdem höher gewichten will, muss selbst eine Preference für QueryComplexityLimiter schreiben, die anstelle des flachen Standardwerts webonyxs fieldComplexity-Mechanismus nutzt, mit dem sich die Kosten eines Feldes sogar in Abhängigkeit von dessen Argumenten berechnen lassen, etwa der angefragten Seitengröße.

Diese Gewichtungstabelle sollte bewusst klein bleiben und sich auf tatsächlich per Profiling identifizierte teure Felder konzentrieren, statt zu versuchen, das gesamte Schema von Hand durchzugewichten. Ein erschöpfendes, manuell gepflegtes Kostenmodell gerät erfahrungsgemäß schnell aus dem Takt mit dem wachsenden Schema und wird selbst zur Wartungslast.


<?php
declare(strict_types=1);

namespace Vendor\GraphQlHardening\Model;

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

/**
 * Ergänzt die Standard-Complexity-Regel um eine höhere Gewichtung
 * für bekannte teure Felder, statt jedes Feld gleich zu zählen.
 */
class WeightedQueryComplexityLimiter
{
    /**
     * Feldname zu zusätzlichem Gewicht, ermittelt per Profiling der teuersten Resolver.
     *
     * @var array<string, int>
     */
    private const FIELD_WEIGHTS = [
        'products' => 15,
        'categoryList' => 8,
    ];

    /**
     * Registriert die gewichteten Validierungsregeln für den GraphQL-Request.
     *
     * @param int $maxDepth
     * @param int $maxComplexity
     * @return void
     */
    public function register(int $maxDepth, int $maxComplexity): void
    {
        DocumentValidator::addRule(new QueryDepth($maxDepth));
        DocumentValidator::addRule(new QueryComplexity($maxComplexity, function (int $childrenComplexity, array $args) {
            $fieldName = $args['fieldName'] ?? '';
            $weight = self::FIELD_WEIGHTS[$fieldName] ?? 1;
            $pageSize = (int) ($args['pageSize'] ?? 1);
            return $childrenComplexity + ($weight * max(1, $pageSize));
        }));
        DocumentValidator::addRule(new DisableIntrospection());
    }
}

8. Zusammenspiel mit Alias-Limit und Introspection: Verteidigung in der Tiefe

Keiner dieser Mechanismen reicht allein aus. Depth und Complexity begrenzen die grundsätzliche Form einer Query, das Alias-Limit begrenzt Feld-Vervielfachungstricks, das Query-Length-Limit begrenzt die reine Payload-Größe, und eine deaktivierte Introspection in Produktion, ebenfalls über QueryComplexityLimiter beziehungsweise die zugehörige Introspection-Konfiguration gesteuert, entzieht Angreifern von vornherein die Möglichkeit, das Schema systematisch nach teuren Feldern zu durchsuchen.

Sinnvoll ist es, diese Mechanismen als sich ergänzende Schichten zu behandeln statt sich auf einen einzigen zu verlassen: Ein öffentlicher Storefront-Endpunkt profitiert von engen Depth- und Complexity-Werten plus den seit 2.4.9 verfügbaren Alias- und Length-Limits plus deaktivierter Introspection, während ein authentifizierter, intern genutzter B2B- oder PWA-Studio-Endpunkt einzelne dieser Schrauben lockern kann, wenn der Traffic ohnehin vertrauenswürdig und überwacht ist.

9. Monitoring und Testing gegen Limit-Verletzungen

Abgelehnte Queries lösen eine GraphQlInputException mit der jeweiligen Verletzungsmeldung von QueryDepth oder QueryComplexity aus. Diese sollten mit ausreichend Kontext geloggt werden, Client-IP, Consumer-Kennung und ein Hash der Query, um einen schlecht implementierten legitimen Client von einem tatsächlichen Angriffsmuster unterscheiden zu können, und bei einem plötzlichen Anstieg der Ablehnungsrate Alarm zu schlagen.

Regressionstests, die GraphQlAbstract erweitern, sollten zwei Dinge gleichzeitig absichern: dass eine legitim tiefe Katalog-Query unter dem konfigurierten Limit weiterhin durchgeht, und dass eine synthetische, bewusst überdimensionierte Query zuverlässig abgelehnt wird. So fällt ein künftiges di.xml-Update sofort auf, wenn es den Schutz unbeabsichtigt aufweicht.

Mechanismus Schützt vor Konfigurierbar über Seit Version
queryDepth (Standard 20) Zu tief verschachtelte Selektionen di.xml (kein Admin-UI) GraphQL-Einführung, ~2.3.1
queryComplexity (Standard 300) Zu viele Felder pro Query di.xml (kein Admin-UI) GraphQL-Einführung, ~2.3.1
Alias-Limit (Standard 10) Feld-Vervielfachung über Aliase Stores > Konfiguration > Services > Magento Web API 2.4.9
Query-Length-Limit (~1 MB) Übergroße Query-Payloads Stores > Konfiguration > Services > Magento Web API 2.4.9
Introspection-Deaktivierung Schema-Reconnaissance durch Angreifer di.xml (Introspection-Konfiguration) GraphQL-Einführung

Mironsoft

Magento-Entwicklung, Modul-Beratung und Systemarchitektur

Magento-Projekt, das eine zweite Meinung oder erfahrene Umsetzung braucht?

Wir entwickeln individuelle Magento-Module, beraten bei Architekturentscheidungen und übernehmen komplexe Umsetzungen, von der Service-Contract-Planung bis zum produktionsreifen Deployment.

Architektur-Beratung

Modul- und Systemarchitektur vor der Umsetzung fundiert durchdenken lassen.

Custom-Modul-Entwicklung

Individuelle Magento-Module nach Best Practices sauber umsetzen.

Code-Review & Audit

Bestehende Module auf Performance, Sicherheit und Wartbarkeit prüfen lassen.

10. Zusammenfassung

Query Complexity Limiting in Magento: Das Wichtigste auf einen Blick

Standardwerte

queryDepth 20 und queryComplexity 300 sind ab Werk aktiv, aber ausschließlich über di.xml einstellbar.

Neu seit 2.4.9

Alias-Limit und Query-Length-Limit lassen sich erstmals direkt im Admin konfigurieren.

Blinder Fleck

Das Standardmodell zählt jedes Feld gleich, teure Resolver brauchen eine eigene Gewichtung.

Empfehlung

Mehrere Mechanismen kombinieren statt sich auf ein einzelnes Limit zu verlassen.

11. FAQ: Query Complexity Limiting in Magento: Das Wichtigste auf einen Blick

1Wo stelle ich queryDepth und queryComplexity in Magento ein?
Nur über di.xml als Konstruktor-Argumente von Magento\Framework\GraphQl\Query\QueryComplexityLimiter, es gibt dafür keine Oberfläche im Admin.
2Was zählt als ein Komplexitätspunkt?
Standardmäßig ein Punkt pro angefragtem Feld, verschachtelte Felder addieren sich in ihre Elternfelder, Fragmente selbst zählen dabei nicht zusätzlich.
3Seit wann gibt es Alias- und Query-Length-Limits im Admin?
Seit Magento 2.4.9, konfigurierbar unter Stores, Konfiguration, Services, Magento Web API, GraphQl Input Limits.
4Kann ich einzelnen Feldern eine höhere Komplexität zuweisen?
Nicht mit Bordmitteln, dafür braucht es eine eigene Preference für QueryComplexityLimiter mit einer fieldComplexity-Zuordnung nach webonyx-Konvention.
5Wie hoch sollte queryComplexity für einen öffentlichen Storefront-Endpunkt sein?
Es gibt keinen Universalwert, sinnvoll ist ein Wert knapp über dem, was echte Storefront-Queries in Produktion tatsächlich benötigen, ermittelt über Logging vor der Umstellung.
6Schützt ein niedriges Complexity-Limit auch vor teuren Einzelfeldern?
Nur indirekt, das Standardmodell gewichtet jedes Feld gleich, ein teures Feld wie eine Facetten-Aggregation zählt genauso viel wie ein einfaches Skalarfeld.
7Muss ich Introspection in Produktion abschalten?
Empfehlenswert für öffentliche Endpunkte, da ein vollständig einsehbares Schema Angreifern das Auffinden teurer Felder erheblich erleichtert.
8Wie kombiniere ich die Limits sinnvoll?
Depth und Complexity für die grundsätzliche Query-Form, Alias- und Length-Limit gegen Vervielfachungstricks und übergroße Payloads, Introspection-Sperre gegen Reconnaissance, als sich ergänzende Schichten.
9Wie erkenne ich, ob ein Limit zu knapp gesetzt ist?
Über Logging abgelehnter Queries mit der jeweiligen Fehlermeldung, ein Anstieg legitimer Ablehnungen nach einem Deploy zeigt meist ein zu enges Limit statt einen Angriff.
10Brauche ich eigene Complexity-Regeln, wenn ich Persisted Queries nutze?
Persisted Queries reduzieren das Risiko erheblich, weil nur bekannte Query-Formen ausgeführt werden, ersetzen aber keine Limits, solange registrierende Clients selbst neue Queries anlegen dürfen.