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.
Inhaltsverzeichnis
- 1. Warum tief verschachtelte und breite Queries eine DoS-Gefahr sind
- 2. Wie ein GraphQL-Server Tiefe und Komplexität überhaupt berechnet
- 3. Magentos eingebaute Limits: queryDepth und queryComplexity
- 4. Admin-UI-Limits seit 2.4.9: Alias- und Query-Length-Begrenzung
- 5. Eigene Depth- und Complexity-Werte per di.xml anpassen
- 6. Grenzen des flachen 1-Punkt-Modells
- 7. Eigene Complexity-Berechnung für kritische Felder ergänzen
- 8. Zusammenspiel mit Alias-Limit und Introspection: Verteidigung in der Tiefe
- 9. Monitoring und Testing gegen Limit-Verletzungen
- 10. Zusammenfassung
- 11. FAQ
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.