Skip to main content

Cómo mitigar los ataques de DOM clobbering en JavaScript

Escrito por

Keshav Malik

7 de agosto de 2023

0 minutos de lectura

El modelo de objetos del documento (DOM) actúa como interfaz entre HTML y JavaScript, y conecta el contenido estático con la interactividad dinámica. Esta función hace que el DOM sea indispensable para los desarrolladores web modernos.

Sin embargo, el DOM tiene un inconveniente: el DOM clobbering. Este ocurre cuando los elementos HTML entran en conflicto con variables o funciones globales de JavaScript, lo que puede provocar un comportamiento inesperado y crear una posible vulnerabilidad de seguridad en tu aplicación web.

Para abordar este problema, se necesitan prácticas de programación rigurosas, conocimientos sólidos de JavaScript y una comprensión firme de la seguridad de las aplicaciones web. En este artículo se explora el concepto de DOM clobbering y se presentan estrategias para crear aplicaciones web más seguras y sólidas.

Cómo se produce el DOM clobbering

Cada elemento HTML puede tener un atributo ID o name único que permite hacer referencia fácilmente a elementos específicos en JavaScript. Por ejemplo, el siguiente botón HTML tiene un atributo ID con el valor "myButton":

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

Podemos usar este ID en nuestro código JavaScript para manipular el botón; por ejemplo, para cambiar el texto cuando se hace clic:

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

El DOM facilita esta interacción entre HTML y JavaScript. Pero ¿qué sucede si el atributo ID o name del elemento HTML entra en conflicto con una variable o función global de JavaScript?

Cuando un navegador carga una página HTML, crea automáticamente variables globales de JavaScript para cada atributo ID y name del DOM HTML. Si tenemos un elemento HTML llamado "myButton", el navegador crea la variable global de JavaScript myButton, que hace referencia a ese elemento HTML.

Ahora, pensemos en una situación en la que declaramos una función de JavaScript llamada myButton:

function myButton() {
    // some code
}

Pero también tenemos un elemento HTML con el ID o name "myButton". Cuando se carga la página, el proceso automático del navegador sobrescribe la función de JavaScript myButton con una referencia al elemento HTML:

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

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

Este proceso es el DOM clobbering y puede causar un comportamiento impredecible y vulnerabilidades de seguridad. Si un atacante puede controlar los atributos, podría inyectar código malicioso en la página web y provocar problemas de seguridad como cross-site scripting (XSS).

Veamos un ejemplo:

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

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

Un atacante podría escribir algo como <img id='alert' src=x onerror='alert(1)'>, para crear un nuevo componente HTML con un 'id' de 'alert'. Este componente reemplaza —o sobrescribe— la función alert normal de JavaScript. La próxima vez que el sitio web intente usar esta función, no funcionará correctamente e incluso podría ejecutar código malicioso.

Cómo identificar vulnerabilidades de DOM clobbering

Imagina una aplicación web básica con un formulario de comentarios. Los usuarios ingresan su nombre y un mensaje, y luego lo envían. La página muestra el comentario:

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>`;
};

Este código toma el nombre y el comentario del usuario y los muestra en un elemento de párrafo dentro del div feedbackDisplay.

Un atacante podría aprovechar este código al enviar HTML en el formulario de comentarios. Por ejemplo, si ingresara el siguiente código en el campo name y enviara el formulario, la etiqueta de script reemplazaría el área donde se muestran los comentarios:

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

Cuando el formulario intente mostrar el siguiente comentario, ejecutará el script en su lugar y redirigirá al usuario a un sitio web malicioso.

Esta situación crea una vulnerabilidad porque la aplicación usa los datos ingresados por el usuario para crear HTML, lo que genera una etiqueta de script con el mismo ID que un elemento existente. Este es un ejemplo de DOM clobbering con consecuencias graves: el atacante puede tomar el control del navegador del usuario y robar datos confidenciales o instalar malware.

Prácticas de programación segura para prevenir el DOM clobbering

Ahora que comprendemos mejor estas vulnerabilidades, veamos algunas prácticas recomendadas para mitigar el riesgo de DOM clobbering.

Delimita correctamente el alcance de las variables y funciones

Una de las causas más comunes del DOM clobbering es el uso incorrecto del alcance global en JavaScript. Si definimos variables y funciones dentro de un alcance específico, podemos limitar las sobrescrituras a ese alcance o a cualquier alcance anidado y minimizar los posibles conflictos.

Apliquemos las reglas de alcance de JavaScript y refactoricemos el ejemplo anterior para ver cómo hacerlo:

(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);
    };
})();

Nota: Usamos DOMPurify para sanitizar el HTML en el bloque de código anterior. Puedes instalarlo en Node.js con npm install dompurify. Inclúyelo en tu HTML con <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.3.3/purify.min.js"></script>.

En esta versión del código, encerramos todo en una expresión de función invocada inmediatamente (IIFE), que crea un nuevo alcance. Las variables form y feedbackElement, así como la función asignada al controlador de eventos onsubmit, no están en el alcance global, por lo que no se pueden sobrescribir mediante DOM clobbering.

Usa identificadores únicos

Asegurarte de que cada elemento de tu página web tenga un ID único reduce el riesgo de sobrescribir por accidente una función o variable importante. Además, evita usar nombres comunes o que puedan entrar en conflicto con objetos o funciones globales de JavaScript.

Evita contaminar el espacio de nombres global

Mantener limpio el espacio de nombres global es fundamental para escribir JavaScript seguro. Cuantas más variables y funciones haya en el alcance global, mayor será el riesgo de DOM clobbering. Usa el alcance de función de JavaScript o el alcance de bloque de ES6 para mantener tus variables y funciones contenidas. Aquí tienes un ejemplo con este último:

    // 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);
    };

En este código, usamos un bloque (delimitado por {}) para crear un nuevo alcance. Ahora, todas las variables y funciones quedan limitadas a este bloque y no están en el alcance global.

Nota: Los ejemplos de código de esta guía están simplificados con fines demostrativos y no están pensados para usarse en un sitio web o una aplicación en producción.

Usa las funciones de JavaScript para mitigar el DOM clobbering

JavaScript moderno ofrece varias funciones que ayudan a minimizar el riesgo de DOM clobbering. En particular, las palabras clave let y const, que se introdujeron en ES6, ofrecen más control al declarar variables.

Tradicionalmente, declaramos variables de JavaScript con la palabra clave var. Sin embargo, var tiene algunas peculiaridades: una de ellas es que no tiene alcance de bloque, sino solo alcance de función y global. Esto significa que se puede acceder a una variable declarada con var y sobrescribirla fuera del bloque en el que se declaró.

Por otro lado, let y const tienen alcance de bloque, lo que significa que solo se puede acceder a ellas dentro del bloque en el que se declararon. Esta característica suele convertirlas en una mejor opción para declarar variables, ya que limita la posibilidad de que se sobrescriban.

También podemos usar const para declarar constantes, es decir, valores que no podemos cambiar después de asignarlos. Son otra herramienta útil para evitar cambios no deseados en variables importantes.

Veamos cómo implementar estas funciones en el código de nuestro formulario de comentarios para prevenir el 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);
    };

En este código, reemplazamos todos los usos de var por const. Limitamos todas las variables al bloque en el que las declaramos y las constantes no se pueden sobrescribir.

Recuerda que usar let y const no elimina el riesgo de DOM clobbering, pero sigue siendo una parte fundamental de la programación segura. Comprender e implementar estas funciones de JavaScript puede hacer que tus aplicaciones web sean más seguras.

Cómo monitorear y detectar vulnerabilidades de DOM clobbering

Las prácticas de programación segura pueden ayudar a prevenir el DOM clobbering, pero detectar las vulnerabilidades cuando aparecen es igual de importante. Veamos algunas herramientas y técnicas que pueden ayudar.

Análisis estático de código

Las herramientas de pruebas estáticas de seguridad de aplicaciones (SAST), como Snyk Code, pueden detectar eficazmente posibles vulnerabilidades de seguridad, incluido el DOM clobbering. Estas herramientas analizan el código fuente sin ejecutarlo e identifican patrones que podrían causar problemas de seguridad. La amplia cobertura de JavaScript de Snyk lo convierte en una excelente opción para los desarrolladores web.

Herramientas para desarrolladores del navegador

Las herramientas para desarrolladores del navegador, como las disponibles en Chrome o Firefox, pueden ser grandes aliadas para detectar vulnerabilidades de DOM clobbering.

Veamos brevemente cómo usar estas herramientas para identificar un posible problema:

  1. En tu página web, haz clic con el botón derecho en cualquier lugar y selecciona Inspeccionar para abrir las herramientas para desarrolladores. También puedes usar Ctrl+Shift+I en Windows o Cmd+Option+I en macOS.

  2. Ve a la pestaña Consola, donde se muestran los errores o registros de JavaScript.

  3. Escribe “window” en la consola y presiona Enter para examinar el alcance global, con todas las variables y funciones globales. Busca variables que parezcan fuera de lugar, especialmente las que tengan el mismo nombre que los ID o nombres de tus elementos HTML.

  4. Usa la pestaña Elementos para editar el HTML de la página, manipular el DOM y probar si hay posibles vulnerabilidades. Por ejemplo, agrega un elemento con un ID que coincida con una variable o función global para ver si se sobrescribe.

Conclusión

En este artículo explicamos cómo ocurre el DOM clobbering, los peligros asociados y cómo protegerse. Delimitar correctamente el alcance de las variables, usar identificadores únicos y elegir adecuadamente las funciones de JavaScript let y const son medidas fundamentales para mitigar esta vulnerabilidad.

Para mantener la seguridad del código, es necesario equilibrar las medidas de prevención y detección. Aplicar prácticas de programación recomendadas, usar las herramientas para desarrolladores del navegador y recurrir a una solución SAST como Snyk puede ayudar a identificar problemas existentes y posibles.

Al incorporar estas técnicas en tus aplicaciones JavaScript y mantenerte al día con las tendencias en seguridad de aplicaciones web, puedes adoptar un enfoque proactivo para proteger tus prácticas de programación.

Snyk para la seguridad de JavaScript

Desde la primera línea de código hasta la última dependencia de npm, Snyk protege tus aplicaciones de JavaScript directamente desde tu IDE, CLI y flujos de trabajo de Git.

Leer más

feature insights context
Blog

Los ataques autónomos ya están aquí. La defensa debe estar a su altura.

Los atacantes autónomos están reduciendo el tiempo disponible para defenderse. Descubre cómo el descubrimiento, la corrección, la validación y la prevención continuos pueden ayudar a los equipos de seguridad a seguirles el ritmo.

illustration hero ai
Blog

El huracán de la IA ha llegado

La IA está acelerando por igual la creación de software y los ciberataques. Los líderes deben proteger los agentes y el código desde el inicio, aplicar controles en tiempo de ejecución y validar las defensas de forma independiente.

feature insights context
Blog

¿La prevención es, en esencia, un problema ya resuelto?

La prevención en el código generado por agentes está resuelta desde el punto de vista arquitectónico, pero elegir controles que protejan la seguridad sin ralentizar el desarrollo sigue siendo el desafío.