Einführung
Stellen Sie sich vor, Sie entwickeln eine Blogging-Webanwendung mit Prisma. Sie schreiben eine einfache Abfrage zur Authentifizierung von Benutzern auf der Grundlage ihrer angegebenen E-Mail und ihres Passworts:
1const user = await prisma.user.findFirst({
2 where: { email, password },
3});
Sieht harmlos aus, oder? Aber was ist, wenn ein Angreifer password = { "not": "" }
? Anstatt das User-Objekt nur zurückzugeben, wenn E-Mail und Passwort übereinstimmen, gibt die Abfrage immer den User zurück, wenn nur die angegebene E-Mail übereinstimmt.
Diese Schwachstelle ist als Operator Injection bekannt, wird aber eher als NoSQL Injection bezeichnet. Was viele Entwickler nicht wissen, ist, dass einige ORMs trotz strenger Modellschemata anfällig für Operator Injection sind , selbst wenn sie mit einer relationalen Datenbank wie PostgreSQL verwendet werden, wodurch das Risiko weiter verbreitet ist als erwartet.
In diesem Beitrag werden wir untersuchen, wie Operator Injection funktioniert, Exploits in Prisma ORM demonstrieren und diskutieren, wie man sie verhindern kann.
Verstehen der Operator Injection
Um die Operatorinjektion in ORMs zu verstehen, ist es interessant, zunächst die NoSQL-Injektion zu betrachten. MongoDB stellte Entwicklern eine API für die Abfrage von Daten mit Operatoren wie $eq
, $lt
und $ne
. Wenn Benutzereingaben blind an die Abfragefunktionen von MongoDB übergeben werden, besteht das Risiko einer NoSQL-Injection.
Beliebte ORM-Bibliotheken für JavaScript begannen, eine ähnliche API für die Abfrage von Daten anzubieten, und inzwischen unterstützen fast alle größeren ORMs eine Variante von Abfrageoperatoren, auch wenn sie MongoDB nicht unterstützen. Prisma, Sequelize und TypeORM haben alle Unterstützung für Abfrageoperatoren für relationale Datenbanken wie PostgreSQL implementiert.
Ausnutzung der Operator-Injektion in Prisma
Prisma-Abfragefunktionen, die mit mehr als einem Datensatz arbeiten, unterstützen in der Regel Abfrageoperatoren und sind anfällig für Injektionen. Zu den Beispielfunktionen gehören findFirst
, findMany
, updateMany
und deleteMany
. Während Prisma die Modellfelder, auf die in der Abfrage verwiesen wird, zur Laufzeit validiert, sind Operatoren eine gültige Eingabe für diese Funktionen und werden daher von der Validierung nicht zurückgewiesen.
Ein Grund, warum Operator Injection in Prisma leicht ausgenutzt werden kann, sind die String-basierten Operatoren, die von der Prisma-API angeboten werden. Einige ORM-Bibliotheken haben die Unterstützung für String-basierte Abfrageoperatoren entfernt, weil sie von Entwicklern so leicht übersehen werden und leicht ausgenutzt werden können. Stattdessen zwingen sie die Entwickler, auf benutzerdefinierte Objekte für Operatoren zu verweisen. Da diese Objekte nicht ohne Weiteres aus Benutzereingaben de-serialisiert werden können, ist das Risiko einer Operation Injection in diesen Bibliotheken stark reduziert.
Nicht alle Abfragefunktionen in Prisma sind anfällig für Operator Injection. Funktionen, die einen einzelnen Datenbankdatensatz auswählen oder ändern, unterstützen in der Regel keine Operatoren und lösen einen Laufzeitfehler aus, wenn ein Objekt angegeben wird. Abgesehen von findUnique akzeptieren auch die Prisma-Funktionen update, delete und upsert keine Operatoren in ihrem where-Filter.
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 });
Bewährte Praktiken zur Verhinderung von Operator Injection
1. Umwandlung von Benutzereingaben in primitive Datentypen
Normalerweise reicht es aus, Eingaben in primitive Datentypen wie Zeichenketten oder Zahlen umzuwandeln, um Angreifer daran zu hindern, Objekte einzuschleusen. Im ursprünglichen Beispiel würde das Casting wie folgt aussehen:
1 const user = await prisma.user.findFirst({
2 where: { email: email.toString(), password: password.toString() },
3 });
2. Benutzereingaben validieren
Auch wenn das Casting effektiv ist, sollten Sie die Benutzereingabe validieren, um sicherzustellen, dass die Eingabe den Anforderungen Ihrer Geschäftslogik entspricht.
Es gibt viele Bibliotheken für die serverseitige Validierung von Benutzereingaben, wie class-validator, zod und joi. Wenn Sie für ein Webanwendungs-Framework wie NestJS oder NextJS entwickeln, empfehlen diese wahrscheinlich spezielle Methoden für die Validierung von Benutzereingaben im Controller.
Im ursprünglichen Beispiel könnte die Validierung von zod wie folgt aussehen:
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. Halten Sie Ihr ORM auf dem neuesten Stand
Bleiben Sie auf dem neuesten Stand, um von den Sicherheitsverbesserungen und -korrekturen zu profitieren . Zum Beispiel hat Sequelize ab Version 4.12 String-Aliase für Abfrageoperatoren deaktiviert, was die Anfälligkeit für Operator-Injection deutlich verringert.
Schlussfolgerung
Operator Injection ist eine echte Bedrohung für Anwendungen, die moderne ORMs verwenden. Die Schwachstelle ergibt sich aus dem Design der ORM-API und hängt nicht mit dem verwendeten Datenbanktyp zusammen. Sogar Prisma in Kombination mit PostgreSQL kann für Operator Injection anfällig sein. Auch wenn Prisma einen gewissen eingebauten Schutz gegen Operator Injection bietet, müssen Entwickler dennoch die Validierung und Bereinigung von Eingaben vornehmen, um die Anwendungssicherheit zu gewährleisten.
Anhang: Prisma-Schema für das Benutzermodell
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}