Skip to main content

Como mitigar ataques de DOM clobbering em JavaScript

Escrito por

Keshav Malik

7 de agosto de 2023

0 minutos de leitura

O Document Object Model (DOM) funciona como uma interface entre HTML e JavaScript, conectando o conteúdo estático à interatividade dinâmica. Por isso, o DOM é indispensável para quem desenvolve para a web hoje.

No entanto, o DOM tem uma armadilha: o DOM clobbering. Isso ocorre quando elementos HTML entram em conflito com variáveis ou funções globais do JavaScript, o que pode causar comportamentos inesperados e criar uma brecha de segurança na sua aplicação web.

Para lidar com esse problema, é preciso adotar boas práticas de programação, conhecer JavaScript a fundo e entender bem a segurança de aplicações web. Este artigo explica o conceito de DOM clobbering e apresenta estratégias para criar aplicações web mais seguras e robustas.

Como ocorre o DOM clobbering

Todo elemento HTML pode ter um atributo ID ou name exclusivo para facilitar sua referência no JavaScript. Por exemplo, o botão HTML a seguir tem um atributo ID com o valor "myButton":

<button id="myButton">Click Me!</button>

Podemos usar esse ID no código JavaScript para manipular o botão, por exemplo, alterando o texto quando alguém clica nele:

document.getElementById('myButton').onclick = function() {
    this.textContent = 'Clicked!';
};

O DOM facilita essa interação entre HTML e JavaScript. Mas o que acontece quando o atributo ID ou name de um elemento HTML entra em conflito com uma variável ou função global do JavaScript?

Quando um navegador carrega uma página HTML, ele cria automaticamente variáveis globais de JavaScript para cada atributo ID e name no DOM do HTML. Se houver um elemento HTML com o nome "myButton", o navegador cria uma variável global de JavaScript, myButton, que faz referência a esse elemento HTML.

Agora, vamos considerar uma situação em que declaramos uma função JavaScript chamada myButton:

function myButton() {
    // some code
}

Mas também temos um elemento HTML com o ID ou name "myButton". Quando a página é carregada, o processo automático do navegador substitui a função JavaScript myButton por uma referência ao elemento HTML:

<button id="myButton">Click Me!</button>

console.log(myButton); // This will log the HTML button element, not the function

Esse processo é chamado de DOM clobbering e pode causar comportamentos imprevisíveis e vulnerabilidades de segurança. Se um invasor puder controlar os atributos, talvez consiga injetar código malicioso na página, causando problemas de segurança como cross-site scripting (XSS).

Para entender melhor, considere a seguinte situação:

Enter your name: <input id="username" type="text">
<button onclick="greet()">Greet</button>

function greet() {
    var username = document.getElementById('username').value;
    alert(`Hello ${username}`);
}

Um invasor poderia digitar algo como <img id='alert' src=x onerror='alert(1)'>, criando um novo componente HTML com um 'id' igual a 'alert'. Esse componente substitui — ou sobrescreve — a função alert padrão do JavaScript. Na próxima vez que o site tentar usar essa função, ela não funcionará corretamente e poderá até executar código malicioso.

Como identificar vulnerabilidades de DOM clobbering

Considere uma aplicação web simples com um formulário para enviar comentários. As pessoas informam o nome e uma mensagem e, em seguida, enviam o formulário. A página exibe o comentário:

HTML:

<h2>Feedback Form</h2>
<form>
  <label for="name">Name:</label><br>
  <input type="text" id="name" name="name"><br>
  <label for="feedback">Feedback:</label><br>
  <textarea id="feedback" name="feedback"></textarea><br>
  <input type="submit" value="Submit">
</form>

<div id="feedbackDisplay"></div>

JavaScript:

document.querySelector('form').onsubmit = function(event) {
    event.preventDefault();

    let name = document.getElementById('name').value;
    let feedback = document.getElementById('feedback').value;

    let feedbackElement = document.getElementById('feedbackDisplay');

    feedbackElement.innerHTML = `<p><b>${name}</b>: ${feedback}</p>`;
};

Esse código pega o nome e o comentário da pessoa e os exibe em um elemento de parágrafo dentro da div feedbackDisplay.

Um invasor poderia explorar esse código enviando HTML no formulário de comentários. Por exemplo, se inserisse o código a seguir no campo name e enviasse o formulário, a área de exibição dos comentários seria substituída pela tag script:

<script id="feedbackDisplay">window.location.href='http://maliciouswebsite.com';</script>

Quando o formulário tentasse exibir o próximo comentário, em vez disso, executaria o script, redirecionando a pessoa para um site malicioso.

Essa situação cria uma vulnerabilidade porque a aplicação usa dados fornecidos pela pessoa para criar HTML, gerando uma tag script com o mesmo ID de um elemento existente. Esse é um exemplo de DOM clobbering com consequências graves: o invasor pode assumir o controle do navegador da pessoa, o que permite roubar dados confidenciais ou instalar malware.

Práticas de programação segura para evitar DOM clobbering

Agora que entendemos melhor essas vulnerabilidades, vamos conhecer algumas boas práticas para reduzir o risco de DOM clobbering.

Defina o escopo correto para variáveis e funções

Uma das causas mais comuns do DOM clobbering é o uso inadequado do escopo global no JavaScript. Ao definir variáveis e funções em um escopo específico, podemos limitar as substituições a esse escopo ou aos escopos aninhados e reduzir possíveis conflitos. 

Vamos aplicar as regras de escopo do JavaScript e refatorar o exemplo anterior para mostrar como fazer isso:

(function() {
    // All variables and functions are now in this function's scope
    const form = document.querySelector('form');
    const feedbackElement = document.getElementById('feedbackDisplay');

    form.onsubmit = function(event) {
        event.preventDefault();

        const name = document.getElementById('name').value;
        const feedback = document.getElementById('feedback').value;

        // Sanitize user input
        name = DOMPurify.sanitize(name);
        feedback = DOMPurify.sanitize(feedback);

        const newFeedback = document.createElement('p');
        newFeedback.textContent = `${name}: ${feedback}`;
        feedbackElement.appendChild(newFeedback);
    };
})();

Observação: usamos DOMPurify para sanitizar o HTML no bloco de código acima. Você pode instalar a biblioteca no Node.js com npm install dompurify. Inclua-a no HTML com <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.3.3/purify.min.js"></script>.

Nesta versão do código, colocamos tudo dentro de uma expressão de função imediatamente invocada (IIFE), que cria um novo escopo. As variáveis form e feedbackElement, assim como a função atribuída ao manipulador de eventos onsubmit, não estão no escopo global e, por isso, não podem ser sobrescritas.

Use identificadores exclusivos

Garantir que cada elemento da página tenha um ID exclusivo reduz o risco de sobrescrever acidentalmente uma função ou variável importante. Além disso, evite nomes comuns ou que possam entrar em conflito com objetos ou funções globais do JavaScript.

Evite poluir o namespace global

Manter o namespace global organizado é essencial para escrever JavaScript seguro. Quanto mais variáveis e funções houver no escopo global, maior será o risco de DOM clobbering. Use o escopo de função do JavaScript ou o escopo de bloco do ES6 para manter variáveis e funções restritas. Veja um exemplo usando o segundo:

    // All variables and functions are now in this block's scope
    let form = document.querySelector('form');
    let feedbackElement = document.getElementById('feedbackDisplay');

    form.onsubmit = function(event) {
        event.preventDefault();

        let name = document.getElementById('name').value;
        let feedback = document.getElementById('feedback').value;

        // Sanitize user input
        name = DOMPurify.sanitize(name);
        feedback = DOMPurify.sanitize(feedback);

        let newFeedback = document.createElement('p');
        newFeedback.textContent = `${name}: ${feedback}`;
        feedbackElement.appendChild(newFeedback);
    };

Neste código, usamos um bloco (definido por {}) para criar um novo escopo. Agora, todas as variáveis e funções ficam restritas a esse bloco e não estão no escopo global.

Observação: os exemplos de código deste guia foram simplificados para fins de demonstração e não devem ser usados em um site ou aplicativo em produção.

Use recursos do JavaScript para mitigar o DOM clobbering

O JavaScript moderno oferece vários recursos que ajudam a reduzir o risco de DOM clobbering. Em particular, as palavras-chave let e const, introduzidas no ES6, dão mais controle sobre a declaração de variáveis.

Tradicionalmente, declaramos variáveis JavaScript com a palavra-chave var. No entanto, var tem algumas particularidades, entre elas, não ter escopo de bloco — apenas escopo de função e global. Isso significa que uma variável declarada com var pode ser acessada e sobrescrita fora do bloco em que foi declarada.

Por outro lado, let e const têm escopo de bloco, ou seja, só podem ser acessadas dentro do bloco em que foram declaradas. Por isso, muitas vezes são opções melhores para declarar variáveis, pois limitam a possibilidade de que elas sejam sobrescritas.

Também podemos usar const para declarar constantes — valores que não podem ser alterados após a atribuição. Elas são mais um recurso útil para evitar mudanças indesejadas em variáveis importantes.

Veja como implementar esses recursos no código do nosso formulário de comentários para evitar DOM clobbering:

    const form = document.querySelector('form');
    const feedbackElement = document.getElementById('feedbackDisplay');

    form.onsubmit = function(event) {
        event.preventDefault();

        const name = document.getElementById('name').value;
        const feedback = document.getElementById('feedback').value;

        // Sanitize user input
        name = DOMPurify.sanitize(name);
        feedback = DOMPurify.sanitize(feedback);

        const newFeedback = document.createElement('p');
        newFeedback.textContent = `${name}: ${feedback}`;
        feedbackElement.appendChild(newFeedback);
    };

Neste código, substituímos todos os usos de var por const. Todas as variáveis ficaram restritas ao bloco em que foram declaradas, e as constantes não podem ser sobrescritas.

Lembre-se de que usar let e const não elimina o risco de DOM clobbering, mas continua sendo uma prática essencial para escrever código seguro. Entender e implementar esses recursos do JavaScript pode tornar suas aplicações web mais seguras.

Como monitorar e detectar vulnerabilidades de DOM clobbering

Práticas de programação segura ajudam a prevenir DOM clobbering, mas detectar vulnerabilidades quando elas surgem é igualmente importante. Vamos conhecer algumas ferramentas e técnicas que podem ajudar.

Análise estática de código

Ferramentas de testes estáticos de segurança de aplicações (SAST), como o Snyk Code, podem detectar vulnerabilidades de segurança em potencial, inclusive DOM clobbering. Essas ferramentas analisam o código-fonte sem executá-lo e identificam padrões que podem levar a problemas de segurança. A cobertura abrangente de JavaScript da Snyk faz dela uma excelente opção para quem desenvolve para a web.

Ferramentas de desenvolvedor do navegador

As ferramentas de desenvolvedor do navegador, como as disponíveis no Chrome ou no Firefox, podem ser grandes aliadas na detecção de vulnerabilidades de DOM clobbering. 

Veja um exemplo rápido de como usar essas ferramentas para identificar um possível problema:

  1. Na sua página web, clique com o botão direito em qualquer lugar e selecione Inspecionar para abrir as ferramentas de desenvolvedor. Você também pode usar Ctrl+Shift+I no Windows ou Cmd+Option+I no macOS.

  2. Acesse a aba Console, onde são exibidos erros ou registros do JavaScript.

  3. Digite “window” no console e pressione Enter para examinar o escopo global e todas as variáveis e funções globais. Procure variáveis que pareçam fora do lugar, especialmente as que tenham o mesmo nome dos IDs ou atributos name dos elementos HTML.

  4. Na aba Elements, edite o HTML da página para manipular o DOM e testar possíveis vulnerabilidades. Por exemplo, adicione um elemento com um ID que corresponda a uma variável ou função global e veja se ele é sobrescrito.

Conclusão

Este artigo explicou como ocorre o DOM clobbering, os riscos associados e como se proteger. Definir o escopo correto das variáveis, usar identificadores exclusivos e escolher adequadamente os recursos let e const do JavaScript são medidas essenciais para mitigar essa vulnerabilidade. 

Manter o código seguro exige equilibrar as iniciativas de prevenção e detecção. Adotar as práticas de programação recomendadas, usar as ferramentas de desenvolvedor do navegador e contar com uma solução SAST como a Snyk ajuda a identificar problemas existentes e potenciais. 

Ao incorporar essas técnicas às suas aplicações JavaScript e acompanhar as tendências de segurança de aplicações web, você pode se antecipar aos riscos e manter suas práticas de programação seguras.

Snyk para a segurança de JavaScript

Da primeira linha de código à última dependência npm, a Snyk mantém seus aplicativos JavaScript seguros diretamente no IDE, na CLI e nos fluxos de trabalho do Git.

Leia mais

feature insights context
Blog

Os ataques autônomos já chegaram. A defesa precisa acompanhar o ritmo.

Os atacantes autônomos estão reduzindo o tempo disponível para a defesa. Saiba como a descoberta, a correção, a validação e a prevenção contínuas ajudam as equipes de segurança a acompanhar esse ritmo.

illustration hero ai
Blog

O furacão da IA chegou

A IA está acelerando tanto a criação de software quanto os ataques cibernéticos. As lideranças devem proteger agentes e código desde o início, aplicar controles em tempo de execução e validar as defesas de forma independente.

feature insights context
Blog

A prevenção é essencialmente um problema resolvido?

A prevenção em código gerado por agentes está resolvida do ponto de vista arquitetural — mas escolher controles que protejam a segurança sem desacelerar o desenvolvimento continua sendo um desafio.