Introduction
Imaginez que vous construisiez une application web de blogging en utilisant Prisma. Vous écrivez une requête simple pour authentifier les utilisateurs sur la base de leur email et de leur mot de passe :
1const user = await prisma.user.findFirst({
2 where: { email, password },
3});
Cela semble inoffensif, n'est-ce pas ? Mais que se passe-t-il si un pirate envoie password = { "not": "" }
? Au lieu de renvoyer l'objet Utilisateur uniquement lorsque l'adresse électronique et le mot de passe correspondent, la requête renvoie toujours l'objet Utilisateur lorsque seule l'adresse électronique fournie correspond.
Cette vulnérabilité est connue sous le nom d'injection d'opérateur, mais elle est plus communément appelée injection NoSQL. Ce que de nombreux développeurs ignorent, c'est qu'en dépit de schémas de modèles stricts , certains ORM sont vulnérables à l' injection d'opérateur même lorsqu'ils sont utilisés avec une base de données relationnelle telle que PostgreSQL, ce qui en fait un risque plus répandu qu'on ne le pense.
Dans ce billet, nous allons explorer le fonctionnement de l'injection d'opérateur, démontrer des exploits dans Prisma ORM, et discuter de la façon de les prévenir.
Comprendre l'injection d'opérateur
Pour comprendre l'injection d'opérateurs dans les ORM, il est intéressant d'examiner d'abord l'injection NoSQL. MongoDB a présenté aux développeurs une API permettant d'interroger les données à l'aide d'opérateurs tels que $eq
, $lt
et $ne
. Lorsque les données de l'utilisateur sont transmises aveuglément aux fonctions d'interrogation de MongoDB, il existe un risque d'injection NoSQL.
Les bibliothèques ORM populaires pour JavaScript ont commencé à offrir une API similaire pour l'interrogation des données et maintenant presque tous les ORM principaux supportent une certaine variation des opérateurs de requête, même s'ils ne supportent pas MongoDB. Prisma, Sequelize et TypeORM ont tous implémenté le support des opérateurs de requête pour les bases de données relationnelles telles que PostgreSQL.
Exploitation de l'injection d'opérateur dans Prisma
Les fonctions de requête Prisma qui opèrent sur plus d'un enregistrement prennent généralement en charge les opérateurs de requête et sont vulnérables à l'injection. Voici quelques exemples de fonctions trouverPremier
, findMany
, updateMany
et deleteMany
. Bien que Prisma valide les champs du modèle référencés dans la requête au moment de l'exécution, les opérateurs sont une entrée valide pour ces fonctions et ne sont donc pas rejetés par la validation.
L'une des raisons pour lesquelles l'injection d'opérateurs est facile à exploiter dans Prisma est la présence d'opérateurs basés sur des chaînes de caractères dans l'API Prisma. Certaines bibliothèques ORM ont supprimé la prise en charge des opérateurs de requête basés sur des chaînes de caractères parce qu'ils sont facilement négligés par les développeurs et faciles à exploiter. Au lieu de cela, elles obligent les développeurs à référencer des objets personnalisés pour les opérateurs. Comme ces objets ne peuvent pas être facilement désérialisés à partir de l'entrée de l'utilisateur, le risque d'injection d'opération est considérablement réduit dans ces bibliothèques.
Toutes les fonctions de requête de Prisma ne sont pas vulnérables à l'injection d'opérateur. Les fonctions qui sélectionnent ou modifient un seul enregistrement de la base de données ne prennent généralement pas en charge les opérateurs et génèrent une erreur d'exécution lorsqu'un objet est fourni. En dehors de findUnique, les fonctions update, delete et upsert de Prisma n'acceptent pas non plus les opérateurs dans leur filtre where.
1 // This query throws a runtime error:
2 // Argument `email`: Invalid value provided. Expected String, provided Object.
3 const user = await prisma.user.findUnique({
4 where: { email: { not: "" } },
5 });
Meilleures pratiques pour prévenir l'injection de l'opérateur
1. Conversion des données utilisateur en types de données primitives
En règle générale, il suffit de convertir les données d'entrée en types de données primitifs tels que les chaînes de caractères ou les nombres pour empêcher les attaquants d'injecter des objets. Dans l'exemple original, la conversion se présente comme suit :
1 const user = await prisma.user.findFirst({
2 where: { email: email.toString(), password: password.toString() },
3 });
2. Valider les données de l'utilisateur
Bien que le casting soit efficace, vous voudrez peut-être valider l'entrée de l'utilisateur pour vous assurer qu'elle répond aux exigences de votre logique d'entreprise.
Il existe de nombreuses bibliothèques pour la validation des données utilisateur côté serveur, telles que class-validator, zod et joi. Si vous développez pour un framework d'application web tel que NestJS ou NextJS, il est probable qu'ils recommandent des méthodes spécifiques pour la validation de l'entrée utilisateur dans le contrôleur.
Dans l'exemple original, la validation de zod pourrait se présenter comme suit :
1import { z } from "zod";
2
3const authInputSchema = z.object({
4 email: z.string().email(),
5 password: z.string().min(8)
6});
7
8const { email, password } = authInputSchema.parse({email: req.params.email, password: req.params.password});
9
10const user = await prisma.user.findFirst({
11 where: { email, password },
12});
3. Maintenez votre ORM à jour
Restez à jour pour bénéficier des améliorations et des corrections de sécurité. Par exemple, Sequelize a désactivé les alias de chaîne pour les opérateurs de requête à partir de la version 4.12, ce qui réduit considérablement la sensibilité à l'injection d'opérateur.
Conclusion
L'injection d'opérateur est une menace réelle pour les applications utilisant des ORM modernes. La vulnérabilité provient de la conception de l'API ORM et n'est pas liée au type de base de données utilisé. En effet, même Prisma combiné à PostgreSQL peut être vulnérable à l'injection d'opérateur. Bien que Prisma offre une protection intégrée contre l'injection d'opérateur, les développeurs doivent toujours pratiquer la validation et l'assainissement des entrées pour garantir la sécurité de l'application.
Annexe : Schéma Prisma pour le modèle de l'utilisateur
1// This is your Prisma schema file,
2// learn more about it in the docs: https://pris.ly/d/prisma-schema
3
4generator client {
5 provider = "prisma-client-js"
6}
7
8datasource db {
9 provider = "postgresql"
10 url = env("DATABASE_URL")
11}
12
13// ...
14
15model User {
16 id Int @id @default(autoincrement())
17 email String @unique
18 password String
19 name String?
20 posts Post[]
21 profile Profile?
22}