GraphQL Security - Angriffe auf GraphQL-APIs
GraphQL-APIs haben spezifische Sicherheitsrisiken: Introspection gibt die gesamte API-Struktur preis, tiefe verschachtelte Queries verursachen Denial-of-Service (Query Depth Attack), Batching ermöglicht Rate-Limit-Bypass, und fehlende Autorisierung auf Field-Ebene führt zu Datenleckagen. Schutz: Introspection in Produktion deaktivieren, Query-Depth-Limits setzen, Field-Level-Authorization implementieren, Query-Complexity-Limiting.
GraphQL ist eine Query-Sprache für APIs die es Clients ermöglicht genau die Daten anzufordern die sie benötigen. Im Vergleich zu REST-APIs bietet GraphQL mehr Flexibilität - aber auch eine erweiterte Angriffsfläche. Die wichtigsten GraphQL-Sicherheitsrisiken entstehen durch Introspection (API-Selbstauskunft), Query-Komplexität (DoS), Batching und fehlende feldgranulare Autorisierung.
GraphQL-Grundlagen und Angriffsfläche
Der zentrale Sicherheitsunterschied zwischen REST und GraphQL liegt in der Struktur: REST-APIs haben feste Endpoints mit klar definierter Datenstruktur und klarer Autorisierung pro Endpoint. GraphQL hat dagegen nur einen einzigen Endpoint, über den beliebige Datenkombinationen abgefragt werden können.
Das bedeutet: Autorisierung muss bei GraphQL auf der Ebene jedes einzelnen Feldes erfolgen. Ein einziges unsichere Field kann alle darüber erreichbaren Daten offenbaren.
Die spezifische Angriffsfläche von GraphQL umfasst folgende Bereiche:
- Introspection: vollständige Schema-Auskunft auf Anfrage
- Query Depth: beliebig tiefe Verschachtelung von Queries
- Query Complexity: beliebig breite Queries mit katastrophaler Performance
- Batching: mehrere Operations in einer einzigen Anfrage
- Field-Level Authorization: fehlende Rechte-Prüfung pro Feld
- SSRF via URL-Felder: serverseitige Requests aus GraphQL-Resolvern
- Injection in Resolver-Argumenten: SQL-Injection, NoSQL-Injection usw.
Introspection - Schema-Reconnaissance
GraphQL bietet eine eingebaute Introspection-Funktion, die das vollständige Datenbankschema auf Anfrage ausliefert - inklusive aller Types, Fields, Mutations und Subscriptions:
{ __schema { types { name fields { name type { name } } } } }
Eine typische Response enthüllt nicht nur die normalen Datenstrukturen, sondern auch sensible Felder wie passwordHash und internalNotes sowie nicht dokumentierte Admin-Bereiche mit Mutationen wie deleteUser und resetAllPasswords.
Tools wie GraphQL Voyager oder InQL (Burp Suite Extension) visualisieren das Schema als interaktiven Graphen und zeigen dem Angreifer die vollständige interne Datenstruktur - einschließlich vergessener oder undokumentierter Admin-Felder.
Weitere Reconnaissance-Queries:
# Alle Queries auflisten:
{ __schema { queryType { fields { name description } } } }
# Alle Mutations:
{ __schema { mutationType { fields { name args { name } } } } }
DoS-Angriffe: Depth und Complexity
Query Depth Attack (rekursive Queries)
Wenn ein Schema rekursive Typen enthält (z.B. User hat friends: [User]), kann ein Angreifer beliebig tiefe Verschachtelungen erzeugen:
{
user(id: 1) {
friends {
friends {
friends {
friends {
friends {
friends { id }
}
}
}
}
}
}
}
10 Ebenen Verschachtelung können zu Millionen von Datenbankabfragen führen und den Server durch Memory Exhaustion oder Timeouts zum Absturz bringen.
Query Complexity Attack (breite Queries)
Auch ohne Rekursion können valide Queries katastrophale Performance erzeugen, indem sie alle Benutzer mit allen Posts, allen Kommentaren und dem jeweiligen Autor in einem einzigen Request abfragen.
Query Batching (Rate-Limit-Bypass)
[
{"query": "mutation { login(email: \"a@b.com\", password: \"pass1\") }"},
{"query": "mutation { login(email: \"a@b.com\", password: \"pass2\") }"},
...1000 mehr...
]
1.000 Login-Versuche ergeben einen einzigen HTTP-Request. Rate-Limiting auf HTTP-Ebene versagt vollständig - Brute-Force-Schutz muss auf der Ebene der GraphQL-Operation implementiert werden.
Authorization-Schwachstellen
Field-Level Authorization Bypass
Der häufigste Fehler: Autorisierung wird nur auf der Ebene der Query geprüft, nicht auf der Ebene einzelner Felder.
type User {
id: ID
name: String
email: String
passwordHash: String # KEIN eigener Auth-Check!
internalNotes: String # Alle eingeloggten User können das lesen?!
creditCardLast4: String
}
Ein eingeloggter Nutzer mit niedriger Berechtigung kann dann abfragen:
{ user(id: 999) { passwordHash creditCardLast4 internalNotes } }
Der Server gibt alle Felder zurück, ohne Fehler zu werfen - und prüft dabei nicht einmal, ob user(id: 999) überhaupt der eigene Account ist.
Horizontal IDOR via GraphQL
Wenn ein Resolver nur prüft ob ein Nutzer eingeloggt ist, aber nicht ob die angeforderten Daten dem Nutzer gehören, sind Horizontal-IDOR-Angriffe möglich:
{ user(id: 2) { email privateMessages { content } } }
Mutation-IDOR
mutation { deletePost(id: 5) }
Dieser Aufruf löscht Post 5 - aber nur wenn der Resolver prüft, ob Post 5 dem eingeloggten Nutzer gehört. Fehlt diese Prüfung, kann jeder Nutzer beliebige Posts löschen.
Erkennung im Pentest
Für GraphQL-Security-Tests stehen spezialisierte Tools zur Verfügung:
| Tool | Zweck |
|---|---|
| InQL (Burp Suite Extension) | Automatische Schema-Extraktion und Testing |
| GraphQL Voyager | Schema-Visualisierung als interaktiver Graph |
| graphw00f | GraphQL Engine Fingerprinting |
| clairvoyance | Schema-Extraktion auch ohne Introspection |
Testing-Methodik
1. Engine Fingerprinting: graphw00f erkennt die verwendete GraphQL-Engine (Apollo, Hasura, GraphQL-Java, Strawberry usw.) - danach bekannte CVEs für die erkannte Engine prüfen.
python3 main.py -f -t https://target.com/graphql
2. Introspection testen:
{ __schema { types { name } } }
Ist eine Response vorhanden, ist Introspection aktiv.
3. Schema-Extraktion ohne Introspection:
python3 main.py -u https://target.com/graphql -w wordlist.txt
clairvoyance sendet viele Queries und errät das Schema aus Fehlermeldungen - funktioniert auch bei deaktivierter Introspection.
4. Depth/Complexity-Test: 10-fach verschachtelte Query absenden und Antwortzeit messen.
5. Batching-Test: 100 Login-Versuche in einem Request senden und prüfen ob alle verarbeitet werden.
6. Authorization-Testing:
- Alle sensitiven Felder auflisten (passwordHash, token, credit, admin)
- Als normaler Nutzer abfragen: werden sie zurückgegeben?
user(id: OTHER_USER_ID)testen: IDOR-Anfälligkeit?- Admin-Mutations ohne Admin-Rechte aufrufen
Schutzmaßnahmen
1. Introspection in Produktion deaktivieren
// Apollo Server:
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production', // Nur DEV!
plugins: [process.env.NODE_ENV === 'production' && ApolloServerPluginLandingPageDisabled()]
});
// graphql-java:
GraphQL.newGraphQL(schema)
.instrumentation(new MaxQueryDepthInstrumentation(10))
.build();
2. Query Depth Limiting
// Node.js (graphql-depth-limit):
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
validationRules: [depthLimit(7)] // Max 7 Ebenen
});
3. Query Complexity Limiting
// Node.js (graphql-query-complexity):
const complexity = getComplexity({
schema,
query: document,
variables,
estimators: [simpleEstimator({ defaultComplexity: 1 })],
maximumComplexity: 1000 // Max 1000 Complexity-Punkte
});
if (complexity > 1000) throw new Error('Query too complex');
4. Batching limitieren oder deaktivieren
// Batching nur wenn unbedingt nötig!
// Kleine Payload-Limits setzen:
app.use('/graphql', bodyParser.json({ limit: '10kb' }));
5. Field-Level Authorization für jedes sensitive Feld
// GraphQL Shield (Node.js):
const permissions = shield({
Query: {
user: isAuthenticated, // Auth auf Query-Ebene
},
User: {
email: isOwnerOrAdmin, // Auth auf Field-Ebene!
passwordHash: deny, // Niemals zugänglich!
internalNotes: isAdmin, // Nur Admins!
}
});
Das Grundprinzip: jedes Feld hat eine eigene Auth-Regel. Standard ist alles verboten, Zugriff muss explizit erlaubt werden.
6. Rate-Limiting auf Operation-Ebene
Rate-Limiting darf nicht nur auf HTTP-Ebene erfolgen, sondern muss für jeden Nutzer und jeden Operation-Typ implementiert werden. Die login-Mutation beispielsweise sollte auf maximal 5 Versuche pro Minute begrenzt sein. Auch Batching muss auf eine maximale Anzahl Operations pro Request beschränkt werden.