Antes de começarmos, gostaria que você analisasse este código e tentasse descobrir qual vulnerabilidade existe nele…

Olhando o código, você poderia pensar que se trata de um path traversal, mas não é, pois o ./
está sendo filtrado. Caso queira testar, pode tentar rodar na sua máquina.
Para entender melhor a vulnerabilidade, vamos revisar as tipagens básicas do JavaScript mas essa vulnerabilidade não ocorre apenas no JavaScript.
A vulnerabilidade de type confusion pode ocorrer em diversas linguagens, especialmente naquelas que permitem casting entre tipos, operações de baixo nível, ou interação com ponteiros.
Aqui vão algumas linguagens além do JavaScript onde o type confusion pode acontecer:
🧠 Linguagens Comuns com Vulnerabilidade de Type Confusion:
✅ C / C++
- Muito comum devido ao uso de ponteiros, casting explícito e falta de verificação de tipos em tempo de execução.
- Exemplo: interpretar uma
struct
como outro tipo, ou cast incorreto de ponteiros.
✅ Java (com reflection ou type erasure)
- Pode acontecer em situações envolvendo generics (type erasure) ou reflection, onde o tipo real é perdido ou trocado.
- Ex: usar
Object
para armazenar algo e depois fazer cast incorreto.
✅ Python (em casos específicos com C extensions ou JIT)
- Em implementações como PyPy ou ao usar Cython/ctypes, você pode causar type confusion ao passar dados mal formatados para código nativo.
✅ Rust (usando unsafe
)
- Rust é seguro por padrão, mas usando o bloco
unsafe
, é possível causar type confusion com transmute, ponteiros brutos, ou FFI (chamadas C).
✅ Go (com interfaces vazias ou unsafe
)
- Go tem interfaces vazias (
interface{}
) que podem ocultar o tipo real. Se mal usado, pode causar confusão de tipo em execuções dinâmicas. - Pacote
unsafe
também permite manipulação de ponteiros e conversões diretas.
✅ Swift e Objective-C
- Especialmente em interoperabilidade entre os dois, onde pode haver confusão de tipo com
id
,Any
, ouunsafeBitCast
.
✅ .NET (C#, VB.NET, etc)
- Reflection, generics e
object
casting podem causar type confusion. - Ferramentas JIT também podem ser vetores de exploração.
👀 Outras Situações:
- Browsers com JIT (ex: V8, Chakra, SpiderMonkey): JS, mas vale lembrar que o type confusion aqui é no nível do engine nativo.
- PHP: raramente, mas pode acontecer com objetos serializados/desserializados maliciosamente.
- WebAssembly: dependendo da linguagem que compila para WASM e o runtime, pode ser possível.
No js quando nos passamos um array ou um objeto os dois se comportam como objeto:

Porque, para o JS, o array é um objeto. E como podemos validar para saber se o objeto é um array ou não?


Então, o type confusion acontece quando o atacante fornece um input de outro tipo do que a aplicação está esperando, e ela não valida o tipo de dado que foi enviado. O type confusion por si só não faz nada muito relevante, mas podemos utilizá-lo para explorar outras partes da aplicação.
Dando uma olhada na documentação do JS e vendo o includes
, podemos entender como ele funciona.

Podemos ver que ele procura por palavras dentro da string

Agora vamos entender como funciona o includes da classe Array.

No array, podemos ver que, ao invés de procurar por strings, ele procura por elementos.

Então, aqui podemos ver que ele procura por elementos e, caso passe uma string, ele não encontra, pois ela não existe.

Agora vamos entender mais um pouco sobre o indexOf da classe String.

O indexOf da string indica o índice da substring procurada.

Agora vamos entender o indexOf do array.

E o indexof no array indica o indice da posição do elemento

Então, agora que entendemos isso, vamos ver como funciona a exploração.

Podemos supor que o usuário tem controle do user_input
.
Vendo o includes
, podemos ver que ele está sendo chamado em uma string.

E já sabemos que o includes de um array é diferente do includes de uma string. Então, se eu transformar minha string em array, conseguimos a flag.

Só que nós temos outra opção aqui também: se passarmos o user_input direto no readFileSync, causamos um erro.

Ele dá erro porque a função espera um arquivo como string, e isso causa um erro no código, ocasionando um DoS.
Mas por que daquela maneira funcionou?
Porque no JS, quando tentamos concatenar um array com uma string, o array é convertido para string.
Então, para explorar, temos duas condições:
A primeira é que a aplicação não está validando a tipagem e está usando o includes
.
A segunda é que ela está concatenando nosso array com uma string.
Agora iremos entender um pouco melhor a função prototype
do JS. O prototype
é onde ficam armazenados diversos métodos e propriedades específicas de uma classe.

Dando uma olhada, podemos ver que ele é um objeto, mas não lista as funções do prototype…

Ao imprimir o includes, ele retorna que é uma função.

Aqui podemos ver que sobrescrevemos o método includes do prototype do array, e isso é feito globalmente.

Então, se fizermos isso com a string, ela retorna true, porque estamos chamando o includes do array, não da string.

Type Confusion – Filter Bypass
Recebemos um IP e, fazendo fuzzing de diretórios, conseguimos encontrar um arquivo ZIP.

Baixando esse arquivo e descompactando, temos acesso ao código-fonte da página.

Analisando o código-fonte da página, conseguimos perceber um diretório e um parâmetro file.

Passando isso na URL, recebemos a mensagem de que precisamos de um parâmetro.

Analisando mais o código, podemos perceber que ele está filtrando o path traversal.

Analisando, podemos ver que ele está usando o includes da string. Então, se transformarmos nossa payload em um array, conseguimos bypassar o filtro e, assim, ler os arquivos do servidor. Ao colocar [] à frente do parâmetro, conseguimos transformar nossa payload em um array.

Assim conseguimos ler arquivos do servidor e pegar a flag.

Type Confusion Attack – ORM (Mongoose)
antes disso precisamos entender o que e um ORM
🧠 O que é o Mongoose?
O Mongoose é uma biblioteca (ODM) usada com Node.js para facilitar o uso do MongoDB.
- Ele permite que você trabalhe com dados do banco de forma orientada a objetos, usando classes, schemas e métodos JavaScript.
- ODM significa Object Document Mapper, porque o MongoDB é um banco baseado em documentos (JSON), não em tabelas como os bancos relacionais.
📄 Como funciona o ORM/ODM do Mongoose
- Conexão
Você conecta seu código ao banco MongoDB:
mongoose.connect('mongodb://localhost:27017/meu_banco');
Aqui você está dizendo: “Use o banco chamado meu_banco
no servidor local (localhost)”.
- Schema (Molde)
Você cria o schema, que é o molde do documento. Por exemplo:
const UsuarioSchema = new mongoose.Schema({
nome: String,
email: String,
idade: Number
});
Isso define que cada documento da coleção usuarios
vai ter:
nome
(texto),email
(texto),idade
(número).
- Model (Objeto que representa a coleção)
A partir do schema, você cria um model:
const Usuario = mongoose.model('Usuario', UsuarioSchema);
Agora você pode usar Usuario
para:
- Criar usuários (
new Usuario(...)
) - Buscar usuários (
Usuario.find()
) - Atualizar e deletar também.
- Document (Instância/Registro)
Criar um novo usuário é assim:
const novo = new Usuario({ nome: 'Maria', email: 'maria@email.com', idade: 30 });
await novo.save(); // salva no banco
Esse novo
é um documento (um registro real da coleção no banco).
📊 Comparando com o SQL:
Conceito SQL | MongoDB / Mongoose |
---|---|
Tabela | Coleção |
Linha (registro) | Documento |
Coluna | Campo |
Esquema (schema) | Schema (do Mongoose) |
ORM (ex: Sequelize) | ODM (Mongoose) |
✅ Em resumo:
O ORM/ODM do Mongoose serve para você interagir com o MongoDB como se estivesse manipulando objetos JS, com validação, segurança e praticidade.
Type Confusion – NoSqli
Logando no lab, temos um arquivo .backup. Baixando-o, conseguimos ver que essa aplicação usa o Mongoose.

Analisando o código da aplicação, podemos ver que temos uma rota para /auth/login utilizando o método POST, que requer dois parâmetros: email e password.

Podemos ver também que, se o tamanho de user for maior que zero, ele retorna a flag.
const { Schema, model } = require("mongoose");
const userSchema = new Schema({
username: {
type: String,
required: true
},
email: {
type: String,
required: true
},
password: {
type: String,
required: true
}
}, {
toJSON: {
transform: function(doc, ret, game) {
delete ret._id;
delete ret.__v;
delete ret.password;
return ret;
}
}
});
const User = model('user', userSchema);
module.exports = User;
Também temos o user.js, que é a forma como o Mongoose está sendo estruturado. Analisando o código, podemos usar o Postman para fazer as requisições à API.

Até agora, não parece haver nenhum tipo de vulnerabilidade nesse código. Então, vamos analisar a documentação do Mongoose e entender como o mongoose.find() funciona.

Aqui podemos ver que ele recebe um objeto como argumento, e nesse objeto é usada a flag $gte: 18. Analisando a documentação do MongoDB, vemos que $gte significa “greater than or equal to” (maior ou igual a), funcionando como um operador de comparação.

$gte é um operador de comparação. No primeiro exemplo, ele verifica se a idade do objeto é maior ou igual a 18 e, se for, executa a ação. Além disso, dentro do $gte, também é possível passar um objeto como parâmetro.
Também tempos a $ne

O $ne
, em vez de pegar o valor igual ao comparado, retorna o valor diferente do especificado. Resumindo, ele é um ≠
.
Podemos especificar objetos dentro do campo que estamos passando… você já deve saber o que dá pra fazer, né? kkkk

Então, conseguimos retornar todos os usuários do banco de dados onde o objeto comparado seja diferente de zero… ou seja, todos.
Mas por que isso acontece?
Primeiro, a aplicação não verifica as variáveis recebidas do JSON, sejam elas strings ou outros tipos. Para mitigar isso, é simples: podemos usar o typeof
.
if(typeof email !== 'string') return res.status(400).json({email: "oia o caba tentando explorar um type confusionkkk"});
if(typeof password !== 'string') return res.status(400).json({password: "oia o caba tentando explorar um type confusionkkk"});

Type Confusion Attack – ORM (Prisma)
O Prisma é um exemplo moderno de ORM muito usado em aplicações Node.js/TypeScript.
✅ O que é o Prisma ORM?
Prisma é um ORM de nova geração que funciona principalmente com bancos de dados relacionais como:
- MySQL
- PostgreSQL
- SQLite
- SQL Server
Ele também suporta MongoDB, mas com algumas limitações, pois o Mongo não é relacional.
🧠 Como o Prisma funciona?
- Definição do modelo de dados Você define os modelos no arquivo
schema.prisma
:model User { id Int @id @default(autoincrement()) name String email String @unique }
- Geração de código (Client) Com
npx prisma generate
, ele gera automaticamente um cliente personalizado para você acessar o banco com código TypeScript. - Acesso ao banco via código Você acessa os dados assim:
const users = await prisma.user.findMany();
Em vez de fazer:SELECT * FROM users;
🔍 Comparações com MongoDB e MySQL
Característica | Prisma com MySQL | Prisma com MongoDB | MongoDB puro (Mongoose) |
---|---|---|---|
Tipo de Banco | Relacional | Não relacional (documentos) | Não relacional (documentos) |
Suporte do Prisma | Completo e maduro | Parcial, ainda em preview/beta | Não usa Prisma, usa Mongoose |
Sintaxe de modelo | Declarativa em schema.prisma | Semelhante, mas adaptada ao MongoDB | Schema do Mongoose em JS |
Operações complexas | Usa relações (@relation , joins) | Menos eficiente para relações | Usa populate , mas não é relacional |
Consulta no código | prisma.user.findMany({}) | prisma.user.findMany({}) | User.find({}) |
Migrations | prisma migrate dev (cria tabelas no SQL) | Não usa migrations (Mongo não tem schema) | Não usa migrations |
🎯 Quando usar Prisma?
Situação | Prisma + MySQL/Postgres | Prisma + MongoDB | MongoDB puro (Mongoose) |
---|---|---|---|
Projeto com dados estruturados (SQL) | ✅ Melhor opção | ❌ Limitado | ❌ Inadequado |
Projeto com dados dinâmicos/flexíveis | ❌ Não ideal | ✅ Possível | ✅ Ideal |
Equipe quer tipagem estática e segurança | ✅ Prisma excelente | ⚠️ Limitado | ❌ Sem tipagem estática |
Suporte a joins complexos | ✅ Sim | ❌ Fraco | ⚠️ Simulado com populate |
🧪 Exemplo rápido
Prisma (MySQL):
model Post {
id Int @id @default(autoincrement())
title String
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
Consulta:
const posts = await prisma.post.findMany({
include: { author: true }
});
MongoDB (Mongoose):
const PostSchema = new Schema({
title: String,
author: { type: Schema.Types.ObjectId, ref: 'User' }
});
Post.find().populate('author');
A nossa vulnerabilidade não se importa muito como o prisma esta estruturado, pós ela e explorada nos métodos do prisma..
pra começa iremos instalar o prisma
❯ npm install express prisma
added 74 packages in 18s
14 packages are looking for funding
run `npm fund` for details
e depois iniciamos um processo com ele

Depois disso, instalei o MySQL para poder usar o Prisma.
❯ docker run -d -p 3306:3306 -e "MYSQL_ROOT_PASSWORD=root" -it mysql
Unable to find image 'mysql:latest' locally
latest: Pulling from library/mysql
c2eb5d06bfea: Pull complete
ba361f0ba5e7: Pull complete
Digest: sha256:2247f6d47a59e5fa30a27ddc2e183a3e6b05bc045e3d12f8d429532647f61358
Status: Downloaded newer image for mysql:latest
56e624751be54a84fd6ec45aedddddd43e6c933cc591bba3cbe7fa5c08c0cc2f
❯ mysql -h 127.0.0.1 -u root -p
Type 'help;' or '\\h' for help. Type '\\c' to clear the current input statement.
MySQL [(none)]> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
4 rows in set (0,013 sec)
MySQL [(none)]> create database type_confusion;
Query OK, 1 row affected (0,017 sec)
Depois de criar a database, voltamos para o arquivo que o Prisma criou e alteramos o .env para ele se comunicar com nosso banco de dados.
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: <https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema>
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: <https://pris.ly/d/connection-strings>
DATABASE_URL="mysql://root:root@127.0.0.1:3306/type_confusion"

E então damos um npx prisma db push para criar os campos da tabela User.

Olhando no mysql nos podemos ver que está lá:
MySQL [type_confusion]> show tables;
+--------------------------+
| Tables_in_type_confusion |
+--------------------------+
| User |
+--------------------------+
1 row in set (0,001 sec)
MySQL [type_confusion]> select * from User;
Empty set (0,001 sec)
MySQL [type_confusion]> desc User;
+----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| username | varchar(191) | NO | | NULL | |
| email | varchar(191) | NO | UNI | NULL | |
| password | varchar(191) | NO | | NULL | |
+----------+--------------+------+-----+---------+----------------+
4 rows in set (0,002 sec)
Depois disso, criamos um app.js que faz a comunicação com o banco, cria usuário, etc.
const { PrismaClient } = require("@prisma/client");
const express = require("express");
const app = express();
const prisma = new PrismaClient();
const port = 8000;
app.post("/create", async (req, res) => {
await prisma.$connect();
const user = await prisma.user.create({
data: {
username: "moluck",
email: "moluck@gmail.com",
password: "storm",
},
});
await prisma.$disconnect();
return res.json(user);
});
app.listen(port, () => {
console.log("running");
});

agora que esta tudo estruturado vamos criar a rota de registro.
const { PrismaClient } = require("./prisma/generated/prisma/");
const express = require("express");
const app = express();
app.use(express.json());
const prisma = new PrismaClient();
const port = 8000;
app.post("/auth/register", async (req, res) => {
const { username, email, password } = req.body;
try {
if (!username)
return res.status(400).json({ username: "This fiel is required" });
if (!email)
return res.status(400).json({ username: "This fiel is required" });
if (!password)
return res.status(400).json({ username: "This fiel is required" });
await prisma.$connect();
const user = await prisma.user.create({
data: {
username,
email,
password,
},
});
await prisma.$disconnect();
return res.json(user);
} catch (error) {
console.error(error);
return res.sendStatus(500);
}
});
app.listen(port, () => {
console.log("running");
});

terminamos de implementar o register. agora vamos para rota de login.
const { PrismaClient } = require("./prisma/generated/prisma/");
const express = require("express");
const app = express();
app.use(express.json());
const prisma = new PrismaClient();
const port = 8000;
app.post("/auth/register", async (req, res) => {
const { username, email, password } = req.body;
try {
if (!username)
return res.status(400).json({ username: "This fiel is required" });
if (!email)
return res.status(400).json({ username: "This fiel is required" });
if (!password)
return res.status(400).json({ username: "This fiel is required" });
await prisma.$connect();
const user = await prisma.user.create({
data: {
username,
email,
password,
},
});
await prisma.$disconnect();
return res.json(user);
} catch (error) {
console.error(error);
return res.sendStatus(500);
}
});
app.post("/auth/login", async (req, res) => {
const { email, password } = req.body;
try {
await prisma.$connect();
const user = await prisma.user.findFirst({
where: {
email,
password,
},
});
await prisma.$disconnect();
if (!user) {
return res.sendStatus(401);
}
return res.json(user);
} catch (error) {
console.error(error);
return res.sendStatus(500);
}
});
app.listen(port, () => {
console.log("running");
});

C @aso eu passe uma senha errada ele não autoriza.

Olhando o código, sabemos que quando passamos o where, ele abrevia o email. Na prática, é algo como:
where: {
email==email
password==email
Então ele não precisa fazer a comparação completa, pois já entende a abreviação… Pelo que vimos até aqui, você já deve saber como explorar a vulnerabilidade, né?
Vamos dar uma olhada no Prisma para ver como os operadores dele funcionam.


Podemos ver que o Prisma tem o operador not, então podemos usá-lo para inverter a comparação com o e-mail. Dessa forma, ele valida e conseguimos retornar o primeiro registro do banco de dados.
Assim, conseguimos explorar o comportamento do Prisma.

Então agora iremos fazer a mitigação da vulnerabilidade.
if (typeof email !== "string")
return res.json({ email: "This field is string" });
if (typeof password !== "string")
return res.json({ password: "This field is string" });

const { PrismaClient } = require("./prisma/generated/prisma/");
const express = require("express");
const app = express();
app.use(express.json());
const prisma = new PrismaClient();
const port = 8000;
app.post("/auth/register", async (req, res) => {
const { username, email, password } = req.body;
try {
if (!username)
return res.status(400).json({ username: "This fiel is required" });
if (!email)
return res.status(400).json({ username: "This fiel is required" });
if (!password)
return res.status(400).json({ username: "This fiel is required" });
await prisma.$connect();
const user = await prisma.user.create({
data: {
username,
email,
password,
},
});
await prisma.$disconnect();
return res.json(user);
} catch (error) {
console.error(error);
return res.sendStatus(500);
}
});
app.post("/auth/login", async (req, res) => {
const { email, password } = req.body;
if (!email)
return res.status(400).json({ username: "This fiel is required" });
if (!password)
return res.status(400).json({ username: "This fiel is required" });
if (typeof email !== "string")
return res.json({ email: "This field is string" });
if (typeof password !== "string")
return res.json({ password: "This field is string" });
try {
await prisma.$connect();
const user = await prisma.user.findFirst({
where: {
email,
password,
},
});
await prisma.$disconnect();
if (!user) {
return res.sendStatus(401);
}
return res.json(user);
} catch (error) {
console.error(error);
return res.sendStatus(500);
}
});
app.listen(port, () => {
console.log("running");
});