Vous subissez une attaque ? Faire appel à notre réponse aux incidents

Développement sécurisé

Sécurité d'une API GraphQL : cinq tests avant le lancement

Cinq tests GraphQL en préproduction : défauts, validation, coût des requêtes, abus de batching et autorisation par champ, chacun avec son correctif.

Aussi en :EnglishTürkçeDeutschEspañolItalianoPolskiNederlandsEesti

Illustration : Sécurité d'une API GraphQL : cinq tests avant le lancement

Exécutez ces cinq tests contre un déploiement de staging de votre API GraphQL avant d'exposer l'endpoint publiquement. Configurez le staging comme le sera la production, faute de quoi les résultats n'ont que peu de valeur.

Ils suivent les quatre domaines du GraphQL Cheat Sheet de l'OWASP : validation des entrées, requêtes coûteuses, contrôle d'accès et configuration par défaut non sécurisée.

1. Vérifier les valeurs par défaut

Les messages d'erreur trop verbeux, l'introspection et GraphiQL sont les réglages par défaut dangereux les plus courants. Testez les trois. Commencez par la plus petite requête d'introspection :

{__schema{types{name}}}

Regardez si le serveur répond avec des détails sur le schéma ou s'il refuse. Cherchez ensuite GraphiQL dans un navigateur. Enfin, envoyez une requête malformée et une requête avec un argument invalide, puis lisez l'erreur renvoyée. Des noms de resolvers, des stack traces ou des messages de base de données dans la réponse constituent une carte pour un attaquant.

Le correctif est une affaire de configuration. Désactivez l'introspection et GraphiQL lorsque l'API n'est pas faite pour être explorée, renvoyez des erreurs génériques et conservez le détail dans les logs côté serveur.

2. Injecter des entrées hostiles dans chaque argument

L'utilisateur fournit des identifiants, puis les data fetchers les emploient dans des requêtes HTTP, des requêtes de base de données et d'autres appels. C'est là que se produisent les injections. Testez chaque argument de chaque query et de chaque mutation avec les payloads que vous utiliseriez sur n'importe quelle autre API : injection SQL et NoSQL, injection de commandes système, SSRF et injection CRLF.

Pour automatiser ces tests, générez les listes d'arguments à partir du schéma tant que l'introspection est activée en staging, faites tourner vos payloads dessus en boucle, puis désactivez l'introspection.

Ce qu'il faut imposer :

  • Des types GraphQL précis, comme les scalaires et les enums, et des schémas définis pour les entrées des mutations.
  • Une liste d'autorisation (allowlist) de caractères, et non une liste de blocage. Une entrée alphanumérique, sans unicode, est un bon point de départ car elle écarte de nombreuses attaques.
  • Des requêtes paramétrées pour tout ce qui est transmis à un interpréteur. Si vous utilisez un ORM ou un ODM, employez-le comme documenté, car l'injection via ORM reste possible.
  • Le rejet des entrées invalides sans révéler le fonctionnement de la validation.

3. Trouver la requête coûteuse

Une API est vulnérable dès lors qu'une limite de ressources est absente ou mal réglée. L'entrée de l'OWASP sur la consommation de ressources non restreinte énumère les délais d'exécution, la mémoire maximale allouable, les descripteurs de fichiers, les processus, la taille des fichiers téléversés, le nombre d'enregistrements par page, le nombre d'opérations par requête cliente et les plafonds de dépenses chez les fournisseurs tiers.

Pour tester, construisez la requête la plus profonde que permet votre schéma en suivant les relations en boucle. Adaptez les noms à votre schéma :

{ users { orders { customer { orders { customer { email } } } } } }

Ajoutez des niveaux jusqu'à ce que le serveur refuse ou ralentisse. Demandez ensuite une taille de page très élevée et envoyez un payload surdimensionné.

Le cheat sheet recommande d'ajouter des contrôles qui limitent ou empêchent les requêtes trop coûteuses. Combinez cela avec un plafond de taille de page, un délai d'exécution maximal et des tailles maximales pour les chaînes, les tableaux et les téléversements. Ajustez les valeurs d'après une semaine de logs de trafic réel, afin que les clients légitimes restent en dessous.

4. Contourner le rate limiter grâce au batching

Le batching, c'est du brute force dont on masque le nombre de requêtes. Dans le deuxième scénario de l'entrée OWASP, une mutation de téléversement est protégée par un rate limiter et un contrôle de taille. L'attaquant envoie un seul POST vers /graphql contenant un tableau de près de mille opérations. Le limiteur ne voit qu'une requête et le serveur manque de mémoire. Si votre API accepte des téléversements, notre article sur la conception et le test de téléversements de fichiers sécurisés traite cet aspect.

Essayez la même approche sur une mutation de connexion ou d'OTP :

[
  {"query": "mutation {login(user: \"alice\", pass: \"guess1\") {token}}"},
  {"query": "mutation {login(user: \"alice\", pass: \"guess2\") {token}}"}
]

Vérifiez si votre limite compte chaque opération du tableau et si les tentatives échouées alimentent bien votre logique de verrouillage. Essayez ensuite tous les autres moyens que votre serveur autorise pour regrouper plusieurs opérations dans une seule requête.

Limitez le nombre d'opérations par requête cliente et restreignez la fréquence à laquelle un même client peut exécuter une opération sensible donnée, comme la validation d'un OTP. Plafonnez aussi les dépenses : dans le premier scénario de l'entrée, un script déclenche des dizaines de milliers de SMS payants via un parcours de mot de passe oublié. Définissez des plafonds de dépenses chez les fournisseurs tiers et des alertes de facturation lorsque les plafonds ne sont pas possibles.

5. Demander le champ d'un autre utilisateur

L'authentification vous dit qui appelle. Elle ne décide pas des champs que cette personne peut lire. Le guide d'autorisation GraphQL indique que l'exécution doit commencer après que le middleware d'authentification a confirmé l'identité, et que le serveur doit quand même décider, pour chaque champ demandé, si l'utilisateur a le droit de le voir.

Testez avec deux comptes ordinaires. Avec la session du compte A, demandez les objets du compte B par leur ID, puis atteignez-les via un objet lié que les deux comptes peuvent voir. Adaptez les noms à votre schéma :

# Direct lookup: should be refused for account A
{ customer(id: "CUSTOMER_ID_OF_B") { email } }

# Same data through a shared parent: must also be refused
{ product(id: "SHARED_PRODUCT_ID") { orders { customer { email } } } }

Si la première requête est refusée et que la seconde renvoie l'e-mail de B, c'est que le contrôle se trouve sur la recherche de premier niveau et n'a jamais été appliqué au type imbriqué. Recommencez sans aucune session. Si les mêmes données sont servies via REST ou une API mobile, testez aussi ces points d'entrée. Notre article sur le test de chaque identifiant de votre application contre les IDOR et BOLA présente la même approche au-delà de GraphQL.

L'autorisation dans un resolver de champ convient pour des prototypes, mais il faut la dupliquer pour chaque point d'entrée. Si les copies divergent, les utilisateurs voient des données différentes selon l'API qu'ils utilisent. Déplacez la logique dans la couche de logique métier afin d'avoir une source de vérité unique. Dans l'exemple du guide, postRepository.getBody reçoit l'utilisateur et le post et ne renvoie le corps qu'à l'auteur. Transmettez un objet utilisateur complet (hydraté), et non un jeton opaque ou une clé d'API.

Ce qu'un scanner ne vous dira pas

Les outils automatisés apportent de la couverture, mais le OWASP Web Security Testing Guide souligne qu'ils sont conçus pour les applications en général, et non pour votre code spécifique et vos règles métier. Les failles les plus graves sont souvent imbriquées dans la logique métier.

Les tests 1 à 3 sont en grande partie automatisables. Le test 5 demande un effort manuel, car il faut que quelqu'un comprenne quel tenant possède quel objet. C'est donc celui qu'il vaut le plus la peine de confier à une équipe externe. Notre service de tests d'intrusion couvre ce besoin.

Par où commencer

  1. Envoyez la requête d'introspection et ouvrez GraphiQL sur le déploiement de staging. Désactivez ce qui répond, puis relancez un test avec une requête malformée pour vérifier les erreurs.
  2. Listez chaque argument de query et de mutation. Vérifiez que chacun a un type ou un enum précis et un validateur par liste d'autorisation.
  3. Envoyez la requête profondément imbriquée, une demande de grande taille de page et un payload surdimensionné. Définissez les limites, puis renvoyez les requêtes et vérifiez que chacune est refusée.
  4. Envoyez un batch sous forme de tableau contre vos mutations de connexion et d'OTP. Imposez une limite par requête et sur les répétitions d'opérations sensibles.
  5. Exécutez le test à deux comptes, relations imbriquées comprises, sur chaque type contenant des données personnelles ou commerciales. Déplacez dans la couche de logique métier tout contrôle trouvé dans un resolver.

Comment nous pouvons vous aider

Tests d’intrusion

Nous attaquons vos réseaux, vos applications et vos équipes comme le ferait un véritable adversaire, puis nous vous montrons comment combler chaque faille.

Nous contacter
Équipe AngelForce

Rédigé par l’équipe AngelForce : des consultants en sécurité certifiés CEH, LPT Master et CISA qui testent, surveillent et protègent les systèmes d’organisations dans toute l’Europe.

Rédigé avec l’aide de l’IA à partir des sources publiques indiquées sur cette page, puis relu par l’équipe AngelForce avant publication.

Sources

  1. cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html
  2. api-security.owasp.org/editions/2023/en/0xa4-unrestricted-resource-consumption/
  3. graphql.org/learn/authorization/
  4. wstg.owasp.org/latest/

Articles : Développement sécurisé

Dites-nous ce que vous devez protéger

Décrivez votre environnement et vos objectifs. Nous vous répondrons par e-mail.

Nous contacter
info@angelforce.eu +372 5705 2044

AngelForce OÜ · Tallinn, Estonie