
Proposta TC39 para mitigar a poluição de protótipos
Autores: Santiago Díaz (Google)
Champion: Shu-yu Guo (Google)
Estágio: 1
Esta proposta procura mitigar uma vulnerabilidade em nível de linguagem conhecida como poluição de protótipo (prototype pollution) com um mecanismo que complementa as primitivas de congelamento e um mecanismo para tornar a maioria das bases de código compatíveis com ela. Ela descreve um recurso opt-in que torna os protótipos disponíveis somente por meio de APIs de reflexão. Com isso, a declaração obj[key] não consegue mais acessar protótipos. Bases de código compatíveis com esse recurso são mais intencionais na forma como usam protótipos.
Vulnerabilidades de PP permitem que atacantes manipulem objetos que eles não controlam ou aos quais não têm acesso em tempo de execução. Essa primitiva de 'ação fantasmagórica à distância' pode ser usada para alterar a forma de outros objetos e sobrescrever suas propriedades, contaminando assim objetos no runtime.
Objetos contaminados invalidam as premissas subjacentes de um código que de outra forma seria seguro/correto e podem levar à execução arbitrária de código e a uma ampla gama de outros problemas de segurança em bases de código JS. Bugs de poluição de protótipo se manifestam com frequência em aplicações web, mas também afetam runtimes JS fora da web.
As propriedades de objetos em JS podem ser gravadas por qualquer código que possa referenciá-las. Em particular, se muitos objetos dependem de uma propriedade compartilhada, qualquer um deles pode efetuar mudanças em todos os outros.
Uma propriedade especial do PP é que ele é um ataque somente com dados, permitindo que a execução de código seja obtida puramente por meio de dados. Por exemplo, veja o seguinte código vulnerável e um exploit correspondente:
// source is attacker-controlled
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
if(target[key] === undefined) {
target[key] = {};
}
target[key] = merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// User input comes as a string
const userSuppliedObj = JSON.parse('{"__proto__": {"polluted": true}}');
// Trigger prototype pollution
merge({}, userSuppliedObj);
// Create a brand new object
const newObj = {};
// Has polluted property
console.log(newObj.polluted); // true
Observe que o exploit consegue contaminar a criação de novos objetos sem injetar nenhum código estranho.
Por causa dessa propriedade especial, mitigações modernas contra problemas de execução de código — como a Content Security Policy ou Trusted Types — ficam aquém da proteção contra PP, pois elas se concentram em impor a proveniência do código.
Nota: ataques somente com dados são relevantes em situações em que o código executado na VM é confiável e a execução arbitrária de código tem impacto de segurança.
freeze, seal e preventExtensionsAs primitivas de congelamento existentes sofrem de problemas de design significativos que tornam improvável sua adoção em larga escala. Elas podem ser úteis para usuários especialistas, mas não são adequadas para serem usadas pela maioria dos desenvolvedores, que razoavelmente esperam que os protótipos sejam mutáveis:
As APIs de congelamento sofrem do erro de sobrescrita e de outras inconsistências que introduzem bugs em bases de código existentes, fazendo-as lançar erros ou, pior, falhar silenciosamente em modo sloppy. Uma investigação anterior sobre o erro de sobrescrita concluiu que ele é acionado em ~10% das bases de código em modo estrito e 20% em modo sloppy. A investigação foi abandonada logo em seguida.
As APIs de congelamento dão aos desenvolvedores a pesada responsabilidade de saber quais protótipos devem ser congelados para manter uma base de código segura, partindo do princípio de que os desenvolvedores são especialistas em segurança. Essas APIs descrevem o quê, mas não o como da segurança. Congelar Object certamente não é suficiente, pois muitos exploits abusam de Array. E Error, Date, Reflect ou Proxy? Ou futuros tipos nativos? As APIs de congelamento não oferecem respostas para essas perguntas.
As APIs de congelamento assumem um ponto de congelamento estável: um momento fixo em tempo de execução em que os protótipos já se estabilizaram e podem ser congelados. Na prática, esse ponto é volátil e muda ao longo do tempo em bases de código em desenvolvimento ativo. Embora seja possível encontrar esse ponto em muitas aplicações hoje, a adição de novas dependências, polyfills, mudanças na estrutura do código e recursos avançados como hotswapping e ferramentas de desenvolvimento fazem dos pontos de congelamento um alvo móvel.
As APIs de congelamento não conseguem proteger toda a cadeia de protótipos. Em JS, objetos podem ser adicionados ou removidos da cadeia de protótipos a qualquer momento. Para proteger toda a cadeia, é preciso lembrar sempre de congelar os objetos adicionados a ela, um processo propenso a erros. Quando são removidos da cadeia, não é mais possível descongelá-los.
Em resumo: um recurso que expõe os protótipos somente a APIs de reflexão. Se os protótipos não fossem disponibilizados por meio de propriedades como __proto__ ou prototype, não estariam expostos a problemas somente com dados.
Isso é melhor compreendido por meio de um exemplo: a declaração obj[one][two] = value é vulnerável a PP por meio de obj.__proto__.polluted. Se alguém deletar a propriedade Object.prototype.__proto__, a mesma declaração não é mais vulnerável, pois não consegue usar o único outro caminho para alcançar protótipos, que é obj.constructor.prototype.polluted. Observe que a propriedade prototype não pode ser deletada.
Esta proposta pode ser implementada fornecendo APIs de reflexão e criando um novo recurso de encapsulamento opt-in que deleta as propriedades dos protótipos. Uma descrição de cada etapa vem a seguir.
__proto__ é um nome de propriedade legado que pode ser deletado, mas o slot interno por trás dele ainda pode ser lido por meio de Object/Reflect.getPrototypeOf e gravado por meio de Object/Reflect.setPrototypeOf, o que simplesmente continuará tornando essa propriedade acessível a código já em execução.
Propomos a criação de novas APIs para prototype, por exemplo getClassPrototypeOf e setClassPrototypeOf, que permitiriam que esse nome de propriedade fosse deletado sem mudar de nenhuma forma como essa propriedade especial funciona e dá suporte à VM.
As APIs de reflexão podem ser polyfilled, o que permite que bases de código endurecidas funcionem em todos os navegadores, inclusive versões mais antigas.
Um novo 'recurso de encapsulamento' opt-in em que nenhum nome de propriedade é criado para as funções getter e setter dos slots de protótipos, o que agora é possível porque as referências a essas propriedades podem usar APIs de reflexão em vez disso.
O recurso é ativado por meio de uma flag fora de banda:
X-Encapsulate-Prototype: true--encapsulate-prototypeQuando o encapsulamento está desativado, os protótipos ficam disponíveis tanto por meio de propriedades quanto por APIs de reflexão.
Quando o encapsulamento está ativado, os protótipos só ficam disponíveis por meio de APIs de reflexão, tendo deletado tanto __proto__ quanto prototype.
O encapsulamento também inclui o seguinte recurso de refatoração automática:
Quando o encapsulamento está ativado, os motores JS que carregam novo código-fonte ativam uma etapa extra em suas fases de parsing que registra todas as notações com ponto para propriedades de protótipo como se fossem chamadas às suas APIs de reflexão. Essa etapa pode ser implementada de forma eficiente e permite que bases de código com dependências de terceiros, transitivas ou carregadas dinamicamente sejam compatíveis com o encapsulamento.
No futuro, essa mudança abrirá caminho para marcar prototype como descontinuado (deprecated).
As propriedades de protótipo poderiam simplesmente ser undefined quando o encapsulamento está ativado, mas poderiam lançar um erro quando houvesse tentativas de leitura/gravação. Isso significaria falhar mais rápido e de forma ruidosa e permitiria testar migrações para as APIs de reflexão/encapsulamento.
Isso implica tornar os getters e setters de __proto__ e prototype condicionais ao encapsulamento, usando um host hook, da mesma forma que a função eval lança erro sob Content Security Policy.
Código que depende de acesso computado a propriedades para referenciar protótipos não é compatível com o encapsulamento nem com a refatoração automática. Ele precisa ser refatorado para referir-se explicitamente a protótipos quando eles são usados. Essa refatoração, na verdade, faz o código expressar intenção, o que torna padrões perigosos visíveis para análise estática. Na prática, bases de código com essa característica costumam ser frameworks de reflexão, ferramentas de depuração e outros casos de uso com uso intensivo de reflexão que provavelmente estão cientes de como usam protótipos.
Bases de código que usam a palavra prototype para definir propriedades personalizadas não são compatíveis. Essas bases de código podem ser tornadas compatíveis com o encapsulamento se essa propriedade for sempre definida/obtida por meio de notação com colchetes. Historicamente, e com base em consultas ao HTTP Archive, há uma pequena porcentagem de bases de código incompatíveis por esse motivo.
Algumas mudanças na propriedade constructor também podem ter ação fantasmagórica à distância. Durante nossa pesquisa, não encontramos vulnerabilidades práticas afetadas por isso.
O nível de exigência para esse ataque funcionar é significativamente alto: como no PP, é preciso encontrar uma aplicação com gadgets para escrever e ler propriedades arbitrárias. Mas na poluição de construtores, o gadget de leitura deve ler de constructor.polluted em vez de polluted. Isso reduz drasticamente o número de gadgets úteis.
Alguns JS minimizados podem ser incompatíveis com o modo de encapsulamento, porque o acesso estático a propriedades pode ser minificado em acesso computado. Consultamos o HTTP Archive para obter uma estimativa disso na prática. A tabela a seguir mostra que páginas com esse comportamento ficam consistentemente abaixo de 1% ao longo dos últimos 12 meses para todas as páginas rastreadas com um navegador de desktop:
| Tabela | Documentos que acessam __proto__ ou constructor dinamicamente | Total de documentos rastreados | Proporção |
|---|---|---|---|
| 2023_03_01_desktop | 5,407,936 | 609,469,458 | 0.89% |
| 2023_02_01_desktop | 4,842,383 | 549,089,708 | 0.88% |
| 2023_01_01_desktop | 5,283,826 | 589,519,160 | 0.90% |
| 2022_12_01_desktop | 5,161,471 | 577,073,883 | 0.89% |
| 2022_11_01_desktop | 5,023,169 | 561,726,239 | 0.89% |
| 2022_10_01_desktop | 4,393,377 | 476,880,624 | 0.92% |
| 2022_09_01_desktop | 4,239,257 | 466,278,762 | 0.91% |
| 2022_08_01_desktop | 4,259,814 | 463,784,047 | 0.92% |
| 2022_07_01_desktop | 3,011,137 | 339,468,615 | 0.89% |
| 2022_06_01_desktop | 2,301,317 | 257,501,222 | 0.89% |
| 2022_04_01_desktop | 2,368,577 | 263,144,657 | 0.90% |
| 2022_03_01_desktop | 2,319,518 | 259,249,013 | 0.89% |
A Google tem observado uma tendência de aumento nos bugs submetidos ao nosso Programa de Recompensas por Vulnerabilidades: 1 em 2020, 3 em 2021 e 5 até agora em 2022. Identificamos vários outros em nossa pesquisa interna.
Exemplos de vulnerabilidades incluem:
Esperamos que o número de aplicações vulneráveis cresça à medida que aplicações JavaScript sejam implantadas em mais ambientes (por exemplo, Electron, Cloudflare Workers etc.). Portanto, uma solução em nível de linguagem é necessária para mitigar ataques em todos os ambientes.