Introducción
Imagina que estás construyendo una aplicación web de blog usando Prisma. Escribes una consulta sencilla para autenticar usuarios basándote en su correo electrónico y contraseña proporcionados:
1const user = await prisma.user.findFirst({
2 where: { email, password },
3});Parece inofensivo, ¿verdad? ¿Pero qué pasa si un atacante envía password = { "not": "" }¿En lugar de devolver el objeto User solo cuando el email y la contraseña coinciden, la consulta siempre devuelve el User cuando solo coincide el email proporcionado?
Esta vulnerabilidad se conoce como inyección de operador, pero es más comúnmente conocida como inyección NoSQL. Lo que muchos desarrolladores no se dan cuenta es que, a pesar de los esquemas de modelo estrictos, algunos ORM son vulnerables a la inyección de operador incluso cuando se utilizan con una base de datos relacional como PostgreSQL, lo que lo convierte en un riesgo más extendido de lo esperado.
En esta publicación, exploraremos cómo funciona la inyección de operadores, demostraremos exploits en Prisma ORM y discutiremos cómo prevenirlos.
Inyección de Operadores
Para entender la inyección de operadores en ORMs, es interesante primero observar la inyección NoSQL. MongoDB introdujo a los desarrolladores una API para consultar datos utilizando operadores como $eq, $lt y $ne. Cuando la entrada del usuario se pasa ciegamente a las funciones de consulta de MongoDB, existe un riesgo de inyección NoSQL.
Las bibliotecas ORM populares para JavaScript empezaron a ofrecer una API similar para consultar datos y ahora casi todos los ORM principales admiten alguna variación de operadores de consulta, incluso cuando no son compatibles con MongoDB. Prisma, Sequelize y TypeORM han implementado soporte para operadores de consulta para bases de datos relacionales como PostgreSQL.
Explotando la inyección de operadores en Prisma
Las funciones de consulta de Prisma que operan en más de un registro suelen admitir operadores de consulta y son vulnerables a la inyección. Algunas funciones de ejemplo incluyen findFirst, findMany, updateMany y deleteMany. Si bien Prisma valida los campos del modelo referenciados en la consulta en tiempo de ejecución, los operadores son una entrada válida para estas funciones y, por lo tanto, no son rechazados por la validación.
Una de las razones por las que la inyección de operadores es fácil de explotar en Prisma es la existencia de operadores basados en cadenas que ofrece la API de Prisma. Algunas bibliotecas ORM han eliminado el soporte para operadores de consulta basados en cadenas porque los desarrolladores los pasan por alto con facilidad y son fáciles de explotar. En su lugar, obligan a los desarrolladores a referenciar objetos personalizados para los operadores. Dado que estos objetos no pueden deserializarse fácilmente a partir de la entrada del usuario, el riesgo de inyección de operaciones se reduce considerablemente en estas bibliotecas.
No todas las funciones de consulta en Prisma son vulnerables a la inyección de operadores. Las funciones que seleccionan o mutan un único registro de base de datos normalmente no admiten operadores y lanzan un error en tiempo de ejecución cuando se proporciona un Objeto. Además de findUnique, las funciones update, delete y upsert de Prisma tampoco aceptan operadores en su filtro '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 });
Mejores prácticas para prevenir la inyección de operadores
1. Convertir la entrada del usuario a tipos de datos primitivos
Normalmente, la conversión de la entrada a tipos de datos primitivos como cadenas o números es suficiente para evitar que los atacantes inyecten objetos. En el ejemplo original, la conversión se vería así:
1 const user = await prisma.user.findFirst({
2 where: { email: email.toString(), password: password.toString() },
3 });2. Validar la Entrada del Usuario
Aunque el casting es efectivo, es posible que desee validar la entrada del usuario para asegurarse de que cumple con los requisitos de su lógica de negocio.
Existen muchas librerías para la validación de entrada de usuario en el lado del servidor, como class-validator, zod y joi. Si estás desarrollando para un framework de aplicaciones web como NestJS o NextJS, es probable que recomienden métodos específicos para la validación de la entrada de usuario en el controlador.
En el ejemplo original, la validación de Zod podría verse así:
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. Mantén tu ORM actualizado
Mantente actualizado para beneficiarte de mejoras y correcciones de seguridad. Por ejemplo, Sequelize deshabilitó los alias de cadena para los operadores de consulta a partir de la versión 4.12, lo que reduce significativamente la susceptibilidad a la inyección de operadores.
Conclusión
La inyección de operadores es una amenaza real para las aplicaciones que utilizan ORMs modernos. La vulnerabilidad se deriva del diseño de la API del ORM y no está relacionada con el tipo de base de datos en uso. De hecho, incluso Prisma combinado con PostgreSQL puede ser vulnerable a la inyección de operadores. Aunque Prisma ofrece cierta protección integrada contra la inyección de operadores, los desarrolladores deben seguir practicando la validación y saneamiento de entradas para garantizar la seguridad de la aplicación.
Apéndice: Esquema de Prisma para el modelo de usuario
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}Protege tu software ahora.


.avif)
