Application Security

Sécuriser GraphQL : Maîtriser la profondeur des requêtes et l'analyse des coûts

GraphQL a révolutionné la façon dont les applications modernes récupèrent les données, offrant aux clients un contrôle précis sur leurs requêtes. Cependant, cette flexibilité comporte un compromis de sécurité majeur : le risque d'attaques par déni de service (DoS) via des requêtes excessivement complexes. Contrairement aux API REST, où les points de terminaison sont prédéfinis, GraphQL permet à un seul point de terminaison d'exécuter presque n'importe quelle structure de requête, ce qui en fait une cible privilégiée pour les acteurs malveillants cherchant à épuiser les ressources du serveur.

Dans cet article, nous explorerons deux stratégies robustes pour atténuer ces risques : l'application de limites de profondeur des requêtes et la mise en œuvre d'une analyse des coûts variables. Ces techniques garantissent que votre API reste réactive et sécurisée sous charge.

Le danger des requêtes non bornées

Considérons une requête simple qui récupère des données utilisateur. Si le schéma autorise des relations récursives (par exemple, un Utilisateur a plusieurs Amis, et chaque Ami est aussi un Utilisateur), un attaquant peut concevoir une requête profondément imbriquée. Au lieu de récupérer dix utilisateurs, il pourrait demander dix amis, puis dix amis pour chacun d'eux, ce qui entraîne une croissance exponentielle du nombre de requêtes à la base de données et de l'utilisation de la mémoire. On appelle souvent cela une attaque par « requête profonde ».

De plus, la récupération de grandes quantités de données en une seule requête peut saturer la bande passante réseau et les pools de connexions de base de données. Pour contrer cela, nous devons mettre en place des garde-fous qui analysent la structure de la requête entrante avant qu'elle n'atteigne votre logique métier.

Stratégie 1 : Imposer des limites de profondeur des requêtes

La limitation de la profondeur des requêtes restreint la profondeur d'imbrication maximale d'une opération. Par exemple, vous pouvez décider qu'une profondeur maximale de 5 est acceptable pour votre application. Toute requête dépassant cette profondeur sera rejetée avec un message d'erreur clair.

La plupart des serveurs GraphQL fournissent des middleware ou des plugins pour gérer cela. Voici un exemple utilisant le package populaire graphql-depth-limit avec un serveur Express.

const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const depthLimit = require('graphql-depth-limit');
const schema = require('./schema');

const app = express();

app.use('/graphql', graphqlHTTP((req) => ({
  schema: schema,
  graphiql: true,
  validationRules: [
    depthLimit(5, 3, ['*'], (depth, paths) => {
      // Optionnel : Journaliser les chemins les plus profonds pour la surveillance
      console.log('Profondeur de requête dépassée :', depth);
    }),
  ],
})));

app.listen(4000, () => console.log('Serveur en cours d\'exécution sur http://localhost:4000'));

Dans cette configuration, le tableau validationRules assure que chaque requête est vérifiée par rapport à la limite de profondeur avant l'exécution. Le troisième argument ['*'] applique la règle à tous les types de requête, tandis que la fonction de rappel vous permet de journaliser les anomalies pour la surveillance de la sécurité.

Stratégie 2 : Analyse des coûts variables

Si la limitation de la profondeur empêche l'imbrication profonde, elle ne tient pas compte du coût de résolution des champs individuels. Un seul champ peut déclencher une jointure de base de données lourde ou un appel à une API externe. L'analyse des coûts attribue un « coût » à chaque champ en fonction de sa complexité computationnelle. Lorsqu'une requête arrive, le serveur calcule le coût total et la rejette si celui-ci dépasse un budget défini.

Cette approche est plus nuancée que la simple limitation de la profondeur. Par exemple, une requête plate récupérant 10 000 identifiants utilisateurs peut avoir une profondeur de 1 mais un coût massif, tandis qu'une requête imbriquée de profondeur 3 avec des champs légers peut être négligeable.

Voici comment vous pourriez implémenter un middleware simple d'analyse des coûts en Node.js :

const { convertToCost } = require('graphql-query-complexity');

// Définir les coûts pour vos résolveurs
const fieldConfig = {
  User: {
    friends: { cost: 10 },
    posts: { cost: 5 },
  },
  Post: {
    comments: { cost: 15 },
  },
};

app.use('/graphql', graphqlHTTP((req) => ({
  schema: schema,
  validationRules: [
    convertToCost({
      schema,
      maximumComplexity: 1000,
      variables: {},
      onComplete: (complexity) => {
        console.log('Complexité de la requête :', complexity);
      },
      createError(max, actual) {
        return new Error(`La requête est trop complexe : ${actual}. Complexité maximale autorisée : ${max}`);
      },
    }),
  ],
})));

Conclusion

La mise en œuvre de la limitation de débit pour GraphQL n'est pas une solution unique. Bien que la limitation de la profondeur fournisse une défense rapide contre les attaques récursives, l'analyse des coûts offre une approche granulaire pour gérer la consommation des ressources. En combinant les deux stratégies, vous créez une couche de défense résiliente qui protège votre infrastructure tout en maintenant la flexibilité qui rend GraphQL puissant.

N'oubliez pas de surveiller régulièrement les journaux de votre API. À mesure que votre schéma évolue, vos limites doivent également évoluer. Des audits réguliers de vos attributions de coûts et de vos seuils de profondeur garantiront que votre application reste sécurisée, performante et prête à évoluer.

Share: