Introdução
Imagine que está a criar uma aplicação Web para blogues utilizando o Prisma. Escreve uma consulta simples para autenticar utilizadores com base no seu e-mail e palavra-passe fornecidos:
1const user = await prisma.user.findFirst({
2 where: { email, password },
3});
Parece inofensivo, certo? Mas e se um atacante enviar password = { "not": "" }
? Em vez de devolver o objeto Utilizador apenas quando o e-mail e a palavra-passe coincidem, a consulta devolve sempre o Utilizador quando apenas o e-mail fornecido coincide.
Esta vulnerabilidade é conhecida como injeção de operador, mas é mais comummente referida como injeção NoSQL. O que muitos programadores não sabem é que, apesar dos esquemas de modelos rigorosos , alguns ORM são vulneráveis à injeção de operadores, mesmo quando são utilizados com uma base de dados relacional como o PostgreSQL, o que torna este risco mais generalizado do que o esperado.
Neste post, exploraremos como funciona a injeção de operador, demonstraremos explorações no Prisma ORM e discutiremos como evitá-las.
Compreender a Injeção de Operador
Para compreender a injeção de operadores em ORM, é interessante olhar primeiro para a injeção NoSQL. O MongoDB apresentou aos desenvolvedores uma API para consulta de dados usando operadores como $eq
, $lt
e $ne
. Quando a entrada do utilizador é passada cegamente para as funções de consulta do MongoDB, existe o risco de injeção NoSQL.
Bibliotecas ORM populares para JavaScript começaram a oferecer uma API semelhante para consulta de dados e agora quase todos os principais ORMs suportam alguma variação de operadores de consulta, mesmo quando não suportam o MongoDB. Prisma, Sequelize e TypeORM implementaram suporte para operadores de consulta para bancos de dados relacionais, como o PostgreSQL.
Explorando a injeção de operador no Prisma
As funções de consulta Prisma que operam em mais de um registo suportam normalmente operadores de consulta e são vulneráveis à injeção. Exemplos de funções incluem findFirst
, findMany
, updateMany
e deleteMany
. Embora o Prisma valide os campos do modelo referenciados na consulta em tempo de execução, os operadores são uma entrada válida para essas funções e, portanto, não são rejeitados pela validação.
Uma razão pela qual a injeção de operador é fácil de explorar no Prisma são os operadores baseados em string oferecidos pela API do Prisma. Algumas bibliotecas ORM removeram o suporte para operadores de consulta baseados em string porque eles são facilmente ignorados pelos desenvolvedores e fáceis de explorar. Em vez disso, obrigam os programadores a referenciar objectos personalizados para os operadores. Como esses objetos não podem ser prontamente desserializados da entrada do usuário, o risco de injeção de operação é bastante reduzido nessas bibliotecas.
Nem todas as funções de consulta no Prisma são vulneráveis à injeção de operador. As funções que selecionam ou alteram um único registo da base de dados normalmente não suportam operadores e lançam um erro em tempo de execução quando é fornecido um objeto. Além de findUnique, as funções Prisma update, delete e upsert também não aceitam operadores em seu 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 });
Melhores práticas para evitar a injeção de operador
1. Converter a entrada do utilizador em tipos de dados primitivos
Normalmente, a conversão da entrada para tipos de dados primitivos, como cadeias de caracteres ou números, é suficiente para evitar que os atacantes injectem objectos. No exemplo original, a conversão seria a seguinte:
1 const user = await prisma.user.findFirst({
2 where: { email: email.toString(), password: password.toString() },
3 });
2. Validar a entrada do utilizador
Embora o casting seja eficaz, poderá querer validar a entrada do utilizador, para garantir que a entrada cumpre os requisitos da lógica empresarial.
Existem muitas bibliotecas para validação do lado do servidor da entrada do utilizador, como class-validator, zod e joi. Se estiver a desenvolver para uma estrutura de aplicações Web, como a NestJS ou a NextJS, é provável que estas recomendem métodos específicos para a validação do input do utilizador no controlador.
No exemplo original, a validação zod pode ter o seguinte aspeto:
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. Mantenha o seu ORM atualizado
Mantenha-se atualizado para se beneficiar das melhorias e correções de segurança. Por exemplo, o Sequelize desativou os aliases de cadeia de caracteres para operadores de consulta a partir da versão 4.12, o que reduz significativamente a suscetibilidade à injeção de operador.
Conclusão
A injeção de operador é uma ameaça real para as aplicações que utilizam ORMs modernos. A vulnerabilidade decorre da conceção da API ORM e não está relacionada com o tipo de base de dados utilizado. Na verdade, mesmo o Prisma combinado com o PostgreSQL pode ser vulnerável à injeção de operador. Embora o Prisma ofereça alguma proteção integrada contra a injeção de operador, os desenvolvedores ainda devem praticar a validação e a sanitização de entrada para garantir a segurança do aplicativo.
Apêndice: Esquema Prisma para o modelo do utilizador
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}