Prevenir Prototype Pollution no Node.js é importante em aplicações que mesclam objetos, interpretam caminhos dinâmicos, processam JSON ou permitem que usuários definam configurações. O ataque modifica propriedades herdadas de objetos JavaScript por meio de chaves como __proto__, constructor e prototype.
Quando uma biblioteca ou função copia dados sem validar essas chaves, um valor controlado pelo atacante pode aparecer em objetos que nunca receberam a propriedade diretamente. Isso pode alterar validações, opções de segurança, comportamento de templates, autorização ou fluxo de execução.
Neste guia, você aprenderá como o ataque funciona, quais padrões são vulneráveis, como bloquear chaves perigosas, usar objetos sem protótipo, validar input, atualizar dependências e testar bypasses.
O que é Prototype Pollution?
Prototype pollution é a alteração indevida de protótipos compartilhados. A referência da OWASP sobre Prototype Pollution explica o risco. A documentação da MDN sobre a cadeia de protótipos ajuda a entender o modelo.
Cadeia de protótipos
Objetos JavaScript normalmente herdam de Object.prototype:
const user = {};
console.log(Object.getPrototypeOf(user) === Object.prototype);Quando uma propriedade não existe diretamente, o runtime procura na cadeia.
Exemplo de impacto
const options = {};
if (options.isAdmin) {
grantAdminAccess();
}Se Object.prototype.isAdmin foi poluído, o teste pode retornar verdadeiro mesmo sem propriedade própria.
Merge vulnerável
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object') {
target[key] ??= {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}Uma entrada com caminhos especiais pode alcançar protótipos.
Payload conceitual
{
"__proto__": {
"isAdmin": true
}
}O comportamento exato depende do parser, da forma de cópia e da versão das bibliotecas, mas a chave deve ser tratada como perigosa.
constructor.prototype
Bloquear apenas __proto__ não basta. Um caminho como:
constructor.prototype.isAdminpode atingir o protótipo em funções que interpretam caminhos separados por ponto.
Função set vulnerável
function setByPath(object, path, value) {
const parts = path.split('.');
let current = object;
for (const part of parts.slice(0, -1)) {
current[part] ??= {};
current = current[part];
}
current[parts.at(-1)] = value;
}Se path vem do usuário, ele pode atravessar chaves proibidas.
Bloqueie chaves perigosas
const forbiddenKeys = new Set([
'__proto__',
'prototype',
'constructor'
]);
function assertSafeKey(key) {
if (forbiddenKeys.has(key)) {
throw new Error('Chave não permitida');
}
}A validação deve ocorrer em todos os níveis e em cada segmento de caminho.
Allowlist é melhor
Quando o formato é conhecido, aceite apenas campos esperados:
const allowed = new Set([
'displayName',
'timezone',
'language'
]);Uma allowlist reduz ataques e erros de mass assignment.
JSON Schema ou Zod
Valide a estrutura antes de copiar:
const SettingsSchema = z.object({
displayName: z.string().max(100),
timezone: z.string(),
language: z.enum(['pt', 'en', 'es'])
}).strict();O modo estrito rejeita propriedades desconhecidas. O artigo sobre Zod será publicado nesta mesma fila.
Object.hasOwn()
Ao verificar flags, use propriedade própria:
if (
Object.hasOwn(options, 'isAdmin')
&& options.isAdmin === true
) {
grantAdminAccess();
}Isso reduz o impacto de propriedades herdadas, mas não corrige a origem da poluição.
Object.create(null)
const dictionary = Object.create(null);
dictionary.userId = '42';Esse objeto não herda de Object.prototype e é útil para dicionários. Entretanto, ele não possui métodos como hasOwnProperty.
Map
Quando as chaves são dinâmicas, prefira Map:
const values = new Map();
values.set(userControlledKey, value);Map não utiliza a cadeia de protótipos para armazenar entradas.
Object.assign
Object.assign é mais previsível que merges recursivos improvisados, mas ainda é necessário validar a entrada e entender setters especiais.
const safe = Object.assign({}, validatedInput);Spread de objeto
const settings = {
...validatedSettings
};O spread não substitui validação. Chaves desconhecidas ainda podem alterar o comportamento da aplicação.
Desestruturação e defaults
const {
timeout = 3000,
retries = 0
} = validatedOptions;Crie um objeto novo apenas com campos aceitos, em vez de repassar todo o input.
Mass assignment
Prototype pollution e mass assignment aparecem juntos quando a API copia o corpo inteiro para o modelo:
await users.update(userId, req.body);Selecione campos explicitamente e não aceite role, isAdmin ou propriedades internas.
Configurações de segurança
Não dependa de defaults em objetos não confiáveis:
const secure = options.secure !== false;Mesmo essa lógica pode ser afetada quando options possui herança inesperada. Valide e crie um objeto limpo.
Bibliotecas vulneráveis
Funções de merge, clone, query string e set por caminho já tiveram falhas desse tipo. Mantenha dependências atualizadas e acompanhe advisories.
Consulte Segurança de Dependências no Node.js quando o artigo entrar na fila.
npm audit
Use scanners como uma camada, não como substituto de revisão. Uma biblioteca pode ser vulnerável apenas em determinado uso.
Dependências transitivas
O pacote vulnerável pode estar vários níveis abaixo. Use lockfile, overrides quando necessário e atualizações testadas.
Query parsers
Parâmetros como:
?__proto__[isAdmin]=truepodem ser interpretados por parsers extensos. Configure profundidade, quantidade de parâmetros e bloqueio de protótipos.
Express e frameworks
Verifique a configuração do parser usado por body, query e form data. Não assuma que todo objeto recebido é seguro.
Template engines
Propriedades poluídas podem alterar opções de escape, caminhos e helpers. Mantenha templates e dados separados.
RCE
Prototype pollution nem sempre executa código diretamente, mas pode formar uma cadeia com um “gadget” vulnerável. Um gadget é uma parte da aplicação que usa uma propriedade herdada de maneira perigosa.
Exemplo de gadget
spawn(command, args, {
shell: options.shell
});Se options.shell vier de protótipo poluído, o comportamento pode mudar. Não use input não confiável em APIs de execução.
Command injection
Mesmo sem pollution, evite shell e valide argumentos. Consulte práticas de processos filhos e segurança de comandos.
Isolamento de autorização
Não baseie autorização em objetos genéricos mesclados. Use identidade validada e consultas explícitas.
Veja RBAC no Node.js e ABAC no Node.js.
Multi-tenancy
Uma propriedade herdada de tenant não deve decidir acesso. O tenant precisa vir da identidade autenticada. Consulte Multi-Tenancy no Node.js.
Congelando protótipos
Object.freeze(Object.prototype);Essa medida pode quebrar bibliotecas e não deve ser aplicada sem testes. Ela também não substitui validação.
Flags do Node.js
Versões do Node.js podem oferecer opções para controlar __proto__. Verifique a documentação da versão usada, mas continue bloqueando chaves perigosas no código.
Processos isolados
Plugins e transformações não confiáveis devem rodar em isolamento real, com permissões limitadas. Prototype pollution em um processo privilegiado pode afetar muitas requisições.
Reinício após incidente
Se um protótipo global foi alterado, remover a propriedade pode não ser suficiente para restaurar todo o estado. Reinicie processos depois de corrigir e investigar.
Logs
Registre rota, campo bloqueado, request ID e tenant, sem armazenar payload completo. Consulte Logs com Pino no Node.js.
Auditoria
Tentativas contra campos de privilégio e alterações de configuração devem ser auditadas. Veja Logs de Auditoria no Node.js.
Teste básico
test('rejeita __proto__', () => {
assert.throws(() => {
parseSettings(JSON.parse(
'{"__proto__":{"isAdmin":true}}'
));
});
assert.equal({}.isAdmin, undefined);
});Teste de constructor.prototype
Teste caminhos como:
constructor.prototype.polluted
prototype.isAdmin
__proto__.enabledTeste de objetos aninhados
As chaves podem aparecer em qualquer profundidade. Gere payloads recursivos e confirme rejeição.
Teste de query string
Envie variações com colchetes, pontos, encoding e parâmetros repetidos.
Teste de dependência
Quando atualizar uma biblioteca de merge ou parser, mantenha um teste de regressão com o payload que antes era vulnerável.
Teste de autorização
Defina uma propriedade em Object.prototype no ambiente de teste e confirme que verificações usam campos próprios e identidade válida.
Monitoramento
Alerte sobre chaves proibidas, aumento de erros de parser e tentativas repetidas. Não confie apenas em bloqueio por WAF.
Erros comuns
- Bloquear apenas __proto__: constructor.prototype permanece.
- Usar for…in: propriedades herdadas são percorridas.
- Merge recursivo próprio: caminhos especiais alcançam protótipos.
- Confiar no scanner: o uso vulnerável continua.
- Autorizar por propriedade herdada: privilégios podem mudar.
- Copiar req.body inteiro: mass assignment aparece.
- Corrigir sem reiniciar: estado poluído pode persistir.
Boas práticas
- Use allowlists de campos.
- Bloqueie __proto__, prototype e constructor.
- Valide cada segmento de caminho.
- Prefira Map para chaves dinâmicas.
- Use Object.create(null) em dicionários.
- Verifique propriedades próprias.
- Atualize dependências.
- Evite merges genéricos.
- Teste payloads codificados.
- Reinicie após incidente.
Conclusão
Prevenir Prototype Pollution no Node.js exige controlar como dados externos viram propriedades. Chaves especiais, caminhos dinâmicos e merges recursivos não podem ser tratados como operações comuns.
Allowlists, objetos limpos, Map, validação estrita e dependências atualizadas reduzem o risco. Quando autorização e configurações usam apenas propriedades próprias e dados validados, uma entrada maliciosa deixa de alterar silenciosamente o comportamento global da aplicação.



