Directive Deception: Exploiting Custom GraphQL Directives for Logic Bypass

Im modernen API-Umfeld wird GraphQL oft als “REST-Killer” bezeichnet, der Entwicklern unvergleichliche Flexibilität und Effizienz bietet. Doch wie das Sprichwort sagt, bringt große Macht auch viele Wege mit sich, um unbeabsichtigt das Backend zu destabilisieren. Eine der ausgeklügeltsten und gleichzeitig übersehenen Angriffsflächen im GraphQL-Ökosystem ist die Directive.
Während Standard-Directives wie @skip und @include im Spezifikationsumfang enthalten sind, liegt die eigentliche Gefahr — und der eigentliche Nutzen — in benutzerdefinierten Directives. Ob sie für @auth, @cache oder @log verwendet werden, diese mächtigen Annotations sitzen oft in einer prekären Position: Sie fungieren als Sicherheits-Middleware, die bei falscher Konfiguration vollständig umgangen werden kann.
In diesem Deep Dive untersuchen wir “Directive Deception” — die Kunst, GraphQL-Directives auszunutzen, um Logik zu umgehen, Sicherheitsprüfungen zu umgehen und Ressourcen zu erschöpfen.
Die Anatomie der GraphQL-Directives: Ein zweischneidiges Schwert
Bevor wir loslegen, müssen wir verstehen, wie sie aufgebaut sind. Eine Directive ist ein Bezeichner, der mit einem @-Zeichen beginnt und an nahezu jeden Teil einer GraphQL-Abfrage oder eines Schemas angehängt werden kann.
1. Schema-Directives vs. Operation-Directives
Schema-Directives: Auf Serverseite definiert, um Typen oder Felder zu dekorieren (z.B. field: String @auth(role: "ADMIN")). Sie werden oft genutzt, um während der Ausführungsphase spezifische Logik auszulösen.
Operation-Directives: Vom Client in der Abfrage selbst gesendet (z.B. query { user @include(if: $isMe) { name } }).
Die Schwachstelle entsteht, wenn benutzerdefinierte Directives als Middleware-Wrapper oder Visitor-Pattern implementiert sind, die das Abfrage-Dokument inspizieren. Wenn die Logik, die diese Directives verarbeitet, die volle Komplexität des GraphQL-AST (Abstract Syntax Tree) nicht berücksichtigt, kann ein Angreifer “blinde Flecken” finden.
Schwachstelle 1: Umgehung von @auth durch Logiküberspringen
Viele Teams implementieren feldbasierte Sicherheit mit einer benutzerdefinierten @auth-Directive. Elegant: Man taggt ein Feld im Schema, und ein “Directive Transformer” stellt sicher, dass nur autorisierte Nutzer es sehen können.
Der Angriff: Die “Geister”-Directive
Das Problem tritt auf, wenn der serverseitige Code nur nach Directives auf den Field-Knoten sucht, Inline-Fragmente oder Fragment-Definitionen aber vergisst zu prüfen.
Betrachten wir diese Abfrage:
query BypassingAuth {
sensitiveData @auth(role: "ADMIN") # Das Middleware erkennt dies und blockiert es
}
Und die täuschende Version:
query StealthyBypass {
... on Query {
sensitiveData # Wenn die Middleware nur Top-Level-Felder prüft, könnte dies durchkommen
}
}
Wenn die Directive-Logik nicht rekursiv ist oder Fragmente nicht vor der Berechtigungsprüfung aufgelöst werden, wird die @auth-Logik nie ausgelöst. Die Ausführungs-Engine sieht einfach ein Feld, das aufgelöst werden muss, und fährt direkt in die Datenbank, wobei die Sicherheitskontrolle umgangen wird.
Warum passiert das?
Entwickler verwenden oft “Schema Visitors”, um Resolver zu umhüllen. Wenn der Visitor nur die fieldDefinition prüft und nicht berücksichtigt, wie der Client das Feld innerhalb eines Fragments neu deklariert, wird die “Wrapper”-Funktion nie auf den spezifischen Ausführungspfad angewandt.
Schwachstelle 2: Directive Injection & Argument Manipulation
“Directive Injection” ist das GraphQL-Äquivalent zu SQL-Injection, das auftritt, wenn eine Anwendung eine GraphQL-Abfrage dynamisch mit unsaniertem Benutzereingaben konstruiert.
Das Szenario: Die Backend-for-Frontend (BFF) Falle
Stellen Sie sich eine BFF vor, die die “Sort”-Präferenz eines Nutzers entgegennimmt und in eine Backend-GraphQL-Abfrage injiziert:
const query = `query { products(sort: "${userInput}") { id name } }`;
Ein Angreifer sendet nicht nur eine Sortier-String, sondern:
"price") @include(if: true) @customDirective(arg: "malicious") #
Die resultierende Abfrage wird:
query { products(sort: "price") @include(if: true) @customDirective(arg: "malicious") #") { id name } }
Durch “Escaping” des Arguments und das Injizieren eigener Directives kann ein Angreifer:
- Logik überschreiben:
@skip(if: true)injizieren, um kritische Felder aus den Logs auszublenden, während Mutationen trotzdem ausgeführt werden. - Interne Directives triggern: Falls das Backend interne Directives wie
@internalDebugoder@bypassCachehat, kann der Angreifer diese nun aufrufen.
Schwachstelle 3: Verschachtelte Fragmente & Auswahlsatz-Umgehung
Directives werden häufig genutzt, um Datenmaskierung oder PII (personenbezogene Daten) zu schützen. Doch die Unterstützung verschachtelter Fragmente in GraphQL kann eine “Maskierungslücke” schaffen.
Wenn eine Sicherheits-Directive auf hoher Ebene angewandt wird, der Angreifer aber ein tief verschachteltes Fragment nutzt, um auf dieselbe Feldreferenz durch einen anderen Beziehungspfad zuzugreifen, wird die hohe Ebene-Directive möglicherweise nicht vererbt.
Der “Kreisreferenz”-Exploit
In vielen Schemas kann man einen User von einem Post aus erreichen und umgekehrt.
Wenn @auth nur auf das User.email-Feld im User-Typ angewandt wird, aber der Entwickler vergessen hat, es auf den Author-Typ anzuwenden (der ein User-Objekt zurückgibt), kann ein Angreifer den langen Weg nehmen:
query DeepEvasion {
post(id: "1") {
author { # Wenn der 'author'-Resolver die 'User'-Directive-Logik nicht auslöst
email
}
}
}
Schwachstelle 4: Cache-Manipulation & Ressourcenerschöpfung 🌀
Die @cache-Directive ist ein Performance-Helden, kann aber auch als DoS-Waffe genutzt werden. Angreifer können Directives verwenden, um den Server zu teuren, nicht gecachten Operationen zu zwingen.
Erzwungene Cache-Misses
Wenn Ihr System eine @cache(ttl: 3600)-Directive nutzt, generiert der Server wahrscheinlich einen Cache-Schlüssel basierend auf dem Abfrage-Hash oder spezifischen Argumenten. Ein Angreifer kann Directive Overloading oder Argument Jittering verwenden, um sicherzustellen, dass jede Anfrage ein Cache-Miss ist.
query Exhaustion($random: String) {
heavyReport(id: "123") @cache(ttl: 0) # Überschreibt die Standard-TTL, falls erlaubt
alias1: heavyReport(id: "123", dummy: $random)
alias2: heavyReport(id: "123", dummy: $random)
}
Indem er einen zufälligen String an ein Dummy-Argument übergibt oder eine Directive injiziert, die den Cache invalidiert, zwingt ein Angreifer den Server, heavyReport (eine teure Operation) mehrfach aufzulösen.
Der Komplexitätsmultiplikator
Wenn der Server die Abfragekomplexität berechnet, werden Directives oft mit einem “Kosten”-Wert versehen, der 0 ist. Doch eine benutzerdefinierte Directive wie @transformImage(size: "ultra-hd") könnte rechenintensiv sein.
Der Angriff: Ein Angreifer sendet eine Abfrage mit Hunderten von Alias-Feldern, die jeweils mit einer “niedrig-kosten”- aber “hoch-wirkungsvollen” Directive dekoriert sind.
Wenn die Komplexität berechnet wird als:
$$Komplexität = \sum_{i=1}^{n} (FeldKosten_i + DirectiveKosten_i)$$
Und $DirectiveKosten$ fälschlicherweise auf 0 gesetzt ist, steigt die tatsächliche Serverbelastung auf $O(n \cdot tatsächlicherImpact)$, was sofort zu Ressourcenerschöpfung führt.
Strategische Abhilfe: Wie Sie Ihr Schema schützen 🛡️
Die Sicherung von Directives erfordert den Übergang von “flacher” Middleware zu “tiefgehender” schema-zentrierter Sicherheit.
1. Vereinheitlichte Directive-Transformation
Verlassen Sie sich niemals auf eine Middleware, die nur die oberste Auswahl prüft. Nutzen Sie einen Directive Transformer, der die Resolver-Map selbst modifiziert. Durch das Wrapping des Resolvers auf Schema-Ebene folgt die Sicherheitslogik dem Feld, egal wie der Client abfragt (über Fragmente, Aliases oder tiefe Verschachtelung).
2. Strenge Directive-Validierung
Begrenzen Sie, welche Directives ein Client senden darf.
- Whitelist: Nur
@includeund@skipvom Client erlauben. - Nur Schema: Sicherstellen, dass Sicherheits-Directives wie
@authnur Schema-Directives sind und nicht vom Client in der Operation überschrieben werden können.
3. Komplexitätszuordnung für Directives
Weisen Sie jeder benutzerdefinierten Directive eine nicht-null Kosten zu. Wenn eine Directive eine Datenbankabfrage oder eine schwere Transformation durchführt, muss sich dies im Kostenmodell widerspiegeln.
Sicherheitstipp: Nutzen Sie eine Kostenanalyse-Bibliothek (wie graphql-cost-analysis), die es erlaubt, einen Multiplikator für Directives festzulegen.
4. Persistierte Abfragen (Die ultimative Verteidigung)
Der effektivste Schutz gegen “Directive Injection” und “Query Manipulation” ist die Verwendung persistierter Abfragen. Durch nur die Ausführung vordefinierter Query-Hashes auf dem Server entziehen Sie Angreifern die Möglichkeit, bösartige Directives zu injizieren oder “tiefe”-Nesting-DoS-Angriffe zu erstellen.
5. Introspektion in Produktion deaktivieren
Obwohl kein Fix für die zugrunde liegende Logik, erschwert das Deaktivieren der Introspektion es Angreifern erheblich, Ihre benutzerdefinierten Directives zu “kartografieren” und jene zu finden, die anfällig für Exploits sind.
Fazit
GraphQL-Directives sind ein Meisterstück extensibler Gestaltung, bringen aber eine Abstraktionsschicht mit sich, in der Sicherheit oft verborgen bleibt — und wo Angreifer gerne spielen. “Directive Deception” ist nicht nur ein einzelner Bug; es geht um das Versäumnis, zu erkennen, dass im GraphQL der Weg zu den Daten genauso wichtig ist wie die Daten selbst.
Indem Sie Ihre Sicherheitslogik in die Resolver integrieren und jede Directive als potenziellen Ressourcenverbraucher behandeln, können Sie ein Graph aufbauen, das sowohl flexibel als auch robust ist.
Related InstaTunnel pages
Continue from this article into the most relevant product guides and workflows.
Related Topics
Keep building with InstaTunnel
Read the docs for implementation details or compare plans before you ship.