In this article
Como evitar vulnerabilidades de poluição de protótipo em JavaScript
A poluição de protótipo é uma vulnerabilidade de JavaScript que permite que invasores adicionem propriedades potencialmente maliciosas aos protótipos de objetos. Isso abre caminho para diferentes tipos de ataques quando objetos definidos pelo usuário herdam esses protótipos.
Neste artigo, você vai entender melhor o que é poluição de protótipo, como ela ocorre, que tipos de ataques pode viabilizar e como evitá-los. Você também vai descobrir como as ferramentas da Snyk ajudam a detectar e corrigir essa vulnerabilidade no seu código e nas dependências.
O que são protótipos em JavaScript
JavaScript usa um conceito chamado herança prototípica. Um protótipo é um objeto com um conjunto de propriedades e funções compartilhadas por todas as variáveis do mesmo tipo. Quando uma variável é declarada em JavaScript, ela acessa automaticamente um ou mais protótipos.
Por exemplo, ao definir uma variável numérica const one = 1, você pode acessar a função toExponential() no protótipo de números. Esse protótipo também tem seu próprio protótipo: o protótipo de objeto. Por isso, mesmo ao trabalhar com um número, você tem acesso a hasOwnProperty(), uma função definida no protótipo de objeto que não foi sobrescrita pelo protótipo de números.
Quando uma variável numérica tem um protótipo que, por sua vez, se conecta a outro protótipo, essa relação é chamada de cadeia de protótipos. No fim, as cadeias de protótipos de todos os tipos se conectam ao protótipo de objeto.
Na maioria das outras linguagens, as relações de herança são definidas em tempo de compilação. Em JavaScript, porém, elas podem ser atualizadas em tempo de execução.
Se você adicionar uma propriedade ou função ao protótipo de uma variável, ela ficará disponível para todas as outras variáveis do mesmo tipo na aplicação, tanto as criadas antes da atualização quanto as criadas depois.
Se você adicionar uma propriedade ou função ao protótipo de objeto, que fica no topo da cadeia de herança, ela ficará disponível para todas as outras variáveis, a menos que seja sobrescrita.
Por exemplo, veja este objeto JavaScript simples:
const myFirstObject = {};
Para descobrir qual é o protótipo desse objeto, você pode usar a palavra-chave __proto__:
myFirstObject.__proto__Você pode usar o código a seguir para criar outro objeto vazio e adicionar uma propriedade ao protótipo dele:
const mySecondObject = {}
mySecondObject.__proto__.customProperty = "this is a property in the prototype"O que acontece se você exibir os protótipos dos dois objetos? Veja o código a seguir:
console.log(myFirstObject.__proto__)
console.log(mySecondObject.__proto__)A saída das duas chamadas a console.log() é idêntica:
[Object: null prototype] { customProperty: 'this is a property in the prototype' }
[Object: null prototype] { customProperty: 'this is a property in the prototype' }A propriedade customProperty que você adicionou ao protótipo do segundo objeto também aparece no protótipo do primeiro. Agora, ela é compartilhada por todos os objetos da sua aplicação.
Se você adicionar um terceiro objeto vazio como este e quiser ver o protótipo dele, use este código:
const myThirdObject = {}
console.log(myThirdObject.__proto__)Você também vai encontrar customProperty nele:
[Object: null prototype] { customProperty: 'this is a property in the prototype' }Se você definir uma propriedade própria em um objeto, ela será usada:
const nonEmptyObject = {
customProperty: "this is a property defined on the object itself"
}
console.log(nonEmptyObject.customProperty) // "this is a property defined on the object itself"Porém, se você usar um objeto com uma propriedade que não foi definida nele, o ambiente de execução do JavaScript vai procurar essa propriedade na cadeia de protótipos do objeto e usar o valor encontrado:
const anotherEmptyObject = {}
console.log(anotherEmptyObject.customProperty) // "this is a property in the prototype"O que é poluição de protótipo em JavaScript
A poluição de protótipo é uma vulnerabilidade que permite que invasores injetem propriedades e funções maliciosas nos protótipos de aplicações JavaScript. No entanto, ela não é um ataque em si, mas um meio de viabilizar diferentes ataques. O protótipo de objeto é o alvo mais comum, pois infectá-lo com código malicioso dá ao invasor o maior alcance possível.
Código JavaScript executado tanto no servidor quanto no cliente pode estar vulnerável a ataques de poluição de protótipo. No servidor, ela costuma viabilizar elevação de privilégios, negação de serviço ou execução remota de código. No cliente, pode abrir caminho para XSS no DOM.
Essa vulnerabilidade pode ocorrer em aplicações escritas em JavaScript e em qualquer linguagem que compile para JavaScript, especialmente TypeScript. Além disso, a poluição de protótipo pode aparecer no seu código e nas dependências, inclusive em algumas versões de bibliotecas populares, como Lodash, collection.js e jQuery.
Imagine que um invasor consiga definir a seguinte propriedade no protótipo de objeto da sua aplicação JavaScript em execução:
isAdmin: true
Se conseguir fazer isso, o invasor poderá procurar no código da sua aplicação verificações de nível de acesso com proteção insuficiente. Se uma verificação depender de um objeto JavaScript que represente o usuário atual e esse objeto não tiver a propriedade isAdmin: false, o ambiente de execução encontrará isAdmin: true no protótipo. A partir daí, será fácil elevar os privilégios e obter acesso a áreas restritas da aplicação.
Mas não se trata apenas de definir novas propriedades em um protótipo. Um invasor também pode sobrescrever funções que já existem nele. Por exemplo, imagine que um invasor consiga sobrescrever a função toString() do protótipo de objeto com uma chamada recursiva:
let newObject = {}
newObject.__proto__.toString = function() {this.toString()}Depois disso, a primeira chamada a toString() em um objeto, ou até mesmo a interpolação de um objeto em uma string de template, derrubará o processo do host com um RangeError: Maximum call stack size exceeded error.
Uma forma comum de invasores poluírem um protótipo é por meio de implementações inseguras de mesclagem recursiva de propriedades no código da aplicação e em bibliotecas.
Poluição de protótipo na prática
Vamos experimentar uma aplicação de exemplo para ajudar você a entender como a poluição de protótipo funciona no Node.js. Você vai trabalhar com uma aplicação Express, um banco de dados MongoDB em memória e alguns endpoints.
Clone este repositório para acompanhar o exemplo. Depois, restaure as dependências e inicie a aplicação:
npm install
npm startAbra http://localhost:8080/todos/ no navegador ou envie uma solicitação GET para http://localhost:8080/todos/ usando seu cliente HTTP favorito. Você receberá um array com três itens de tarefas:
[
{
"_id": "654c062468efa31d8c055c13",
"text": "Jason's first todo item",
"open": true,
"visible": true,
"owner": "654c062468efa31d8c055c0e",
"__v": 0
},
{
"_id": "654c062468efa31d8c055c14",
"text": "First todo for Kelly",
"open": true,
"visible": true,
"owner": "654c062468efa31d8c055c0f",
"__v": 0
},
{
"_id": "654c062468efa31d8c055c15",
"text": "Paul's thing to be done",
"open": true,
"visible": true,
"owner": "654c062468efa31d8c055c10",
"__v": 0
}
]
Agora, se você abrir database/seed.js no repositório clonado, verá que o banco de dados da aplicação foi inicializado não com três, mas com quatro itens de tarefas:
const seedTodoItems = [
{
text: "Jason's first todo item",
open: true,
visible: true,
owner: "Jason"
},
{
text: "First todo for Kelly",
open: true,
visible: true,
owner: "Kelly"
},
{
text: "Paul's thing to be done",
open: true,
visible: true,
owner: "Paul"
},
{
text: "A HIDDEN TODO",
open: true,
owner: "Alexandra"
}
]
Três tarefas iniciais estão marcadas explicitamente como visíveis, mas uma delas não está, ou seja, fica oculta por padrão. Se você abrir server.js e conferir o endpoint que atende à solicitação GET, verá que ele retorna apenas as tarefas visíveis:
app.get('/todos/', (req, res) => {
TodoItem.find({visible: true})
.then(data => res.json(data))
.catch(error => res.json({error}))
})Use outro endpoint para adicionar uma nova tarefa. Envie a seguinte solicitação POST para http://localhost:8080/todos/add:
POST http://localhost:8080/todos/add
Content-Type: application/json
{
"__proto__": {
"visible": true
},
"text": "😈 new todo item that tries to mess with the prototype",
"owner": "654c0cc960d82b5802a0b30d"
}Você pode usar a seguinte solicitação curl na linha de comando para criar uma nova tarefa:
curl -X POST -H 'content-type: application/json' http://localhost:8080/todos/ad
d --data '{ "__proto__": {
"visible": true
},
"text": "😈 new todo item that tries to mess with the prototype",
"owner": "654c0cc960d82b5802a0b30d"
}'Agora, envie novamente uma solicitação GET para http://localhost:8080/todos/. Veja o que será retornado:
[
{
"_id": "654c0d59654aeeb63dfd5ae8",
"text": "Jason's first todo item",
"open": true,
"visible": true,
"owner": "654c0d59654aeeb63dfd5ae3",
"__v": 0
},
{
"_id": "654c0d59654aeeb63dfd5ae9",
"text": "First todo for Kelly",
"open": true,
"visible": true,
"owner": "654c0d59654aeeb63dfd5ae4",
"__v": 0
},
{
"_id": "654c0d59654aeeb63dfd5aea",
"text": "Paul's thing to be done",
"open": true,
"visible": true,
"owner": "654c0d59654aeeb63dfd5ae5",
"__v": 0
},
{
"_id": "654c0d59654aeeb63dfd5aeb",
"text": "A HIDDEN TODO",
"open": true,
"owner": "654c0d59654aeeb63dfd5ae6",
"__v": 0
},
{
"_id": "654c0e6f654aeeb63dfd5aef",
"text": "😈 new todo item that tries to mess with the prototype",
"open": true,
"owner": "654c0cc960d82b5802a0b30d",
"__v": 0
}
]
No início, havia três tarefas visíveis. Você adicionou mais uma. Mas agora há cinco, e não quatro. A tarefa que deveria ficar oculta agora está visível!
Vamos ver o que acontece no endpoint que adiciona uma nova tarefa:
app.post('/todos/add', (req, res) => {
const defaults = {
open: true,
}
const todoToAdd = lodash.merge(defaults, req.body)
const todoItem = new TodoItem(todoToAdd);
todoItem.save()
.then(() => res.json({msg: `Successfully added a todo item`}))
.catch(error => res.json({error: error.message}))
})
A aplicação analisa o corpo da solicitação POST (um objeto JSON) e o mescla ao objeto defaults, que é usado para iniciar cada nova tarefa. Para fazer a mesclagem, ela usa o método merge() de uma versão vulnerável do Lodash. Como resultado, a parte maliciosa da solicitação que manipula o protótipo de objeto é mesclada sem problemas ao objeto resultante e, nesse processo, define Object.__proto__.visible como true.
Como resultado, os objetos existentes e futuros que não declararem explicitamente a propriedade visible começarão a herdá-la do protótipo, que agora a contém. Ao consultar o banco de dados durante uma solicitação GET, a aplicação usa um filtro para recuperar apenas as tarefas visíveis:
app.get('/todos/', (req, res) => {
TodoItem.find({visible: true})
.then(data => res.json(data))
.catch(error => res.json({error}))
})
Ao avaliar o filtro, descobrimos que a tarefa oculta não tem sua própria propriedade visible: false:
{
text: "A HIDDEN TODO",
open: true,
owner: "Alexandra"
}Porém, agora o protótipo tem visible: true. O valor dessa propriedade é usado, expondo sem querer uma tarefa preexistente que deveria ficar oculta. Além disso, a tarefa adicionada na solicitação maliciosa também é exibida, embora não tenha visible: true definido.
Como evitar vulnerabilidades de poluição de protótipo
Na aplicação de exemplo anterior, basta atualizar o Lodash para uma versão mais recente para corrigir a vulnerabilidade. Porém, há outras formas de evitar a poluição de protótipo que você precisa conhecer, especialmente ao analisar seu próprio código, e não apenas código de uma biblioteca.
Sanitize as chaves das propriedades
A forma mais óbvia de resolver o problema é verificar se uma chave é segura para mesclar antes de fazer a mesclagem:
if (key == '__proto__') {
return;
}No entanto, manter uma lista de bloqueio não é o ideal, pois há formas conhecidas de contornar essa proteção.
Uma opção melhor é manter uma lista de permissões com as chaves de propriedades permitidas:
if (allowedKeys.includes(key)) {
// Proceed with merge
}Use Map em vez de um objeto
JavaScript oferece uma alternativa aos objetos para armazenar pares de chave e valor: o Map. Embora seja parecido com um objeto em muitos aspectos, há uma diferença importante: ao usar a função get() de um Map, você só consegue obter valores que adicionou explicitamente a ele. Assim, itens maliciosos que poderiam ter sido adicionados ao protótipo de objeto não serão acessados:
const defaultsMap = new Map()
defaultsMap.set("open", true);
defaultsMap.set("hidden", false)
// vs
const defaultsObject = {
open: true,
hidden: false
}Use Object.freeze() ou Object.seal() para impedir alterações
JavaScript oferece duas funções integradas que limitam as alterações que podem ser feitas em objetos: Object.freeze() e Object.seal(). Como o protótipo de objeto também é um objeto, você pode usar essas funções nele:
Object.freeze(Object.prototype)Para evitar a poluição de protótipo, prefira Object.freeze(), pois essa função impede tanto a adição de novas propriedades quanto a modificação das existentes.
Object.seal() impede apenas a adição de novas propriedades, mas permite atualizar as existentes. Portanto, se você usar Object.seal(), ainda será possível poluir o protótipo ao sobrescrever funções do protótipo de objeto, como toString().
Use o pacote nopp da Snyk
Uma boa extensão do conceito de congelamento de objetos é o pacote nopp da Snyk. Ele aplica Object.freeze() a alguns dos objetos integrados do JavaScript. Quando usado no fim da inicialização da aplicação, permite alterações legítimas nos protótipos, mas bloqueia tentativas maliciosas de poluição quando a aplicação já está em execução.
Na aplicação Express de exemplo usada anteriormente, basta instalar o nopp:
npm install noppE adicioná-lo como última instrução de importação em server.js:
import express from 'express';
import connect from "./database/connection.js";
import seedDatabase from "./database/seed.js";
import TodoItem from "./model/todoitem.model.js";
import lodash from "lodash";
import cors from "cors";
import "nopp";Assim que o nopp é incluído, o cenário de poluição de protótipo para elevação de privilégios é mitigado, mesmo que a aplicação de exemplo continue usando uma versão vulnerável do Lodash.
Use a extensão da Snyk para IDEs para detectar vulnerabilidades de poluição de protótipo
Vale lembrar que vulnerabilidades, incluindo a poluição de protótipo, são mais fáceis de corrigir quando são fáceis de detectar. Uma ferramenta que destaque possíveis problemas de segurança no editor de código pode ajudar muito você a entregar código JavaScript seguro.
Se uma ferramenta assim parece interessante, considere instalar a extensão Snyk Security para Visual Studio Code. (Se você trabalha com IDEs JetBrains, como o WebStorm, também há uma extensão para você.)
A extensão da Snyk identifica e destaca possíveis vulnerabilidades de poluição de protótipo e outros tipos no seu código JavaScript e nas bibliotecas. Para cada vulnerabilidade detectada, ela mostra como projetos de código aberto corrigiram problemas semelhantes.
Veja a seguir uma captura de tela da extensão da Snyk para VS Code em ação. Ela detecta várias vulnerabilidades no pacote npm lodash, incluindo poluição de protótipo, negação de serviço por expressão regular e injeção de comandos:

Para instalar a extensão do Snyk, pesquise por "snyk" no painel de extensões do Visual Studio Code:

Instale a extensão chamada "Snyk Security - Code, Open Source Dependencies, IaC Configurations":

A extensão do Snyk é instalada junto com o Snyk CLI, necessário para executar as verificações do Snyk daqui em diante.
Agora, clique no ícone do Snyk na barra de menus à esquerda do Visual Studio Code. No painel do Snyk, clique em Trust workspace e conecte-se:

O Snyk abre uma janela do navegador para você entrar na sua conta do Snyk ou criar uma:

Depois que você entrar, o Snyk precisa autenticar sua máquina para associar o Snyk CLI local à sua conta do Snyk:

Depois que você clicar em Authenticate, o aplicativo web do Snyk confirma que a autenticação foi concluída:

Agora, você pode fechar a janela do navegador e voltar ao Visual Studio Code. Ao abrir um workspace ou uma pasta, o Snyk inicia a análise de vulnerabilidades.
Veja como o Visual Studio Code pode ficar depois que a extensão do Snyk analisa um projeto:

No painel “SNYK”, à esquerda, você verá uma lista de problemas de segurança e qualidade de código identificados, incluindo poluição de protótipo, negação de serviço por expressão regular (ReDoS), falsificação de solicitação entre sites (CSRF) e exposição de informações.
Na guia “Editor”, as instruções de código afetadas aparecem sublinhadas. Ao pressionar Ctrl +. (ou Cmd + . no Mac), um menu com ações rápidas relacionadas é exibido. Em um painel separado, a extensão do Snyk resume a vulnerabilidade detectada e mostra exemplos de como vulnerabilidades semelhantes foram corrigidas em vários projetos de código aberto.
A flexibilidade do JavaScript tem um preço, e a poluição de protótipo é um ótimo exemplo de como é fácil explorar a linguagem quando os desenvolvedores não adotam as medidas necessárias para proteger o código.
A melhor maneira de aprender sobre segurança de aplicações é praticando programação. Aproveite a lição do Snyk Learn sobre poluição de protótipo, com exemplos interativos de código!

As extensões de IDE do Snyk Security ajudam você a detectar poluição de protótipo e outras vulnerabilidades desde o início, sem precisar sair do seu editor de código favorito.
Usar as partes mais seguras do JavaScript e ferramentas inteligentes para desenvolvedores ajuda você a lançar código JavaScript seguro e evitar incidentes de segurança dispendiosos.
Comece a proteger seus aplicativos JavaScript
Encontre e corrija vulnerabilidades em JavaScript gratuitamente com o Snyk.
Não é necessário informar um cartão de crédito.
Ou cadastre-se com o Azure AD Docker ID Bitbucket
Ao usar o Snyk, você concorda em cumprir nossas políticas, incluindo os Termos de Serviço e a Política de Privacidade.