Introducción
Imagina que estás creando una aplicación web de blogging con Prisma. Escribes una consulta sencilla para autenticar a los usuarios basándote en su correo electrónico y contraseña:
1const user = await prisma.user.findFirst({
2 where: { email, password },
3});
Parece inofensivo, ¿verdad? Pero ¿y si un atacante envía password = { "not": "" }
? En lugar de devolver el objeto Usuario sólo cuando coinciden el correo electrónico y la contraseña, la consulta siempre devuelve el Usuario cuando sólo coincide el correo electrónico proporcionado.
Esta vulnerabilidad se conoce como inyección de operador, pero se conoce más comúnmente como inyección NoSQL. Lo que muchos desarrolladores no saben es que, a pesar de los estrictos esquemas de modelos , algunos ORM son vulnerables a la inyección de operadores 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 este post, exploraremos cómo funciona la inyección de operadores, demostraremos exploits en Prisma ORM y discutiremos cómo prevenirlos.
Entender la inyección de operadores
Para entender la inyección de operadores en los ORM, es interesante fijarse primero en 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 librerías ORM populares para JavaScript empezaron a ofrecer una API similar para consultar datos y ahora casi todos los ORM importantes soportan alguna variación de los operadores de consulta, incluso cuando no soportan MongoDB. Prisma, Sequelize y TypeORM han implementado soporte para operadores de consulta para bases de datos relacionales como PostgreSQL.
Explotación de la inyección de operadores en Prisma
Las funciones de consulta de Prisma que operan sobre más de un registro suelen soportar operadores de consulta y son vulnerables a la inyección. Algunos ejemplos de funciones son encontrarPrimero
, findMany
, updateMany
y deleteMany
. Aunque Prisma valida los campos del modelo a los que se hace referencia 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 razón por la que la inyección de operadores es fácil de explotar en Prisma, son los operadores basados en cadenas que ofrece la API de Prisma. Algunas librerías ORM han eliminado el soporte para operadores de consulta basados en cadenas porque son muy fáciles de pasar por alto por los desarrolladores y fáciles de explotar. En su lugar, obligan a los desarrolladores a hacer referencia a objetos personalizados para los operadores. Como estos objetos no pueden ser fácilmente de-serializados a partir de la entrada del usuario, el riesgo de inyección de operaciones se reduce en gran medida en estas bibliotecas.
No todas las funciones de consulta en Prisma son vulnerables a la inyección de operadores. Las funciones que seleccionan o modifican un único registro de la base de datos normalmente no admiten operadores y arrojan un error de ejecución cuando se proporciona un objeto. Aparte de findUnique, las funciones Prisma update, delete y upsert 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 });
Buenas prácticas para evitar la inyección de operadores
1. Convertir entradas de usuario en tipos de datos primitivos
Normalmente, para evitar que los atacantes inyecten objetos, basta con convertir la entrada a tipos de datos primitivos como cadenas o números. En el ejemplo original, la conversión sería la siguiente:
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 eficaz, es posible que desee validar la entrada del usuario, para asegurarse de que la entrada cumple con sus requisitos de lógica de negocio.
Hay muchas librerías para la validación del lado del servidor de la entrada del usuario, 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 del usuario en el controlador.
En el ejemplo original, la validación de zod podría tener el siguiente aspecto:
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. Mantenga actualizado su ORM
Manténgase actualizado para beneficiarse de las mejoras y correcciones de seguridad. Por ejemplo, Sequelize desactivó 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 ORM modernos. La vulnerabilidad proviene del diseño de la API del ORM y no está relacionada con el tipo de base de datos utilizado. 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 el saneamiento de entradas para garantizar la seguridad de la aplicación.
Apéndice: Esquema 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}