Neutron, nuestro motor de IA, obtuvo un 96.75% en el benchmark CyberGym de UC Berkeley. Más información

Ingeniería

Ingeniería

Mejora de la detección de XSS en PostMessage mediante instrumentación con objetos Proxy

El artículo presenta un nuevo método para detectar vulnerabilidades de cross-site scripting (XSS) en PostMessage mediante objetos Proxy de JavaScript, que mejora las técnicas tradicionales de fuzzing dinámico.

Introducción

Las vulnerabilidades de cross-site scripting (XSS) siguen siendo una amenaza persistente. Aprovechan la naturaleza dinámica de las aplicaciones web para ejecutar scripts maliciosos. Entre la multitud de canales a través de los cuales pueden facilitarse los ataques XSS, la API postMessage de HTML5 destaca por su uso generalizado para permitir la comunicación entre orígenes.

Sin embargo, esta utilidad también abre una caja de Pandora de vulnerabilidades, en particular cuando no se valida con rigor. Aunque los métodos de detección tradicionales son eficaces hasta cierto punto, a menudo se quedan cortos a la hora de identificar y mitigar con precisión este tipo de vulnerabilidades, sobre todo en entornos web complejos y generados dinámicamente.

Este artículo presenta un enfoque novedoso para mejorar la detección de vulnerabilidades XSS en PostMessage. Aprovecha las capacidades de los objetos Proxy de JavaScript para la instrumentación, combinadas con una fase de perfilado previa al fuzzing dinámico.

Al integrar la instrumentación con objetos Proxy en el perfilado preliminar de las aplicaciones web, se prepara el terreno para un proceso de fuzzing más dirigido y eficiente. Este enfoque agiliza la detección de vectores XSS sutiles.

Las secciones siguientes profundizan en los detalles técnicos de este enfoque y en su implementación. Con esta exploración, buscamos dotar a los desarrolladores, a los profesionales de la seguridad y a los investigadores de un conjunto de herramientas sólido para proteger las aplicaciones web frente a la amenaza siempre presente de los ataques XSS.

Contexto

La API postMessage, parte integral de HTML5, revolucionó las comunicaciones web al permitir el paso de mensajes entre orígenes. Diseñada para facilitar la interacción entre documentos de distintos orígenes, desempeña un papel fundamental en la web moderna y está presente en todo, desde los widgets de terceros hasta las aplicaciones de una sola página más complejas. Sin embargo, su flexibilidad y su potencia la convierten en un objetivo de explotación, principalmente mediante ataques de cross-site scripting (XSS).

Los ataques XSS consisten en inyectar scripts maliciosos en páginas web que visualizan otros usuarios, aprovechando la confianza que un usuario deposita en un sitio determinado. Las vulnerabilidades XSS tradicionales surgen de entradas de usuario mal saneadas que se incluyen directamente en el contenido de la página web. La API postMessage introduce un nuevo vector para estos ataques al enviar mensajes entre orígenes, lo que puede incluir contenido malicioso que se ejecute en el contexto de la página receptora.

Vulnerabilidades y errores comunes

Validación del origen

Una de las principales vulnerabilidades asociadas a postMessage es la falta de validación del origen. Cuando se recibe un mensaje, no comprobar su origen o implementar mal esta comprobación puede llevar a aceptar mensajes de fuentes maliciosas. El siguiente ejemplo muestra una implementación vulnerable:

window.addEventListener('message', (event) => {

  // Dangerous: No check for `event.origin`

  eval(event.data);

});

En este fragmento, el listener del evento de mensaje ejecuta de forma indiscriminada el código contenido en cualquier mensaje recibido, sin verificar su origen. Este enfoque abre la puerta a la ejecución de código JavaScript arbitrario, lo que lo convierte en un objetivo prioritario para los ataques XSS.

Ejemplo de código con validación del origen.

window.addEventListener('message', (event) => {
  // Securely checking the origin of the message
  if (event.origin === 'https://trusted-origin.com') {
    // Assuming the content is safe, further validation can also be implemented here
    eval(event.data);
  } else {
    console.error('Untrusted origin:', event.origin);
  }
});

Cross-site scripting

El XSS en PostMessage se produce cuando una aplicación gestiona de forma incorrecta los datos recibidos a través de la API postMessage, lo que lleva a la ejecución de código no confiable y potencialmente malicioso. Esta vulnerabilidad suele deberse a dos descuidos habituales:

  • Falta de validación del origen: no validar el origen del mensaje entrante puede permitir que los atacantes envíen mensajes maliciosos desde fuentes no confiables.
  • Saneamiento incorrecto del contenido del mensaje: tratar el contenido de los mensajes recibidos como una entrada confiable, sin un saneamiento adecuado, puede llevar a la ejecución de código JavaScript arbitrario.

Consideremos una aplicación web que escucha mensajes para actualizar dinámicamente el contenido en función de los datos recibidos a través de postMessage:

window.addEventListener('message', (event) => {
  // Assume the content of the message is a URL to be navigated to
  if (event.data.url) {
    window.location.href = event.data.url; // Potential for exploitation
  }
});

En este escenario, un atacante podría crear un mensaje que contenga una URL de JavaScript (javascript:), lo que provocaría la ejecución de código arbitrario:

parent.postMessage({url: "javascript:alert('XSS')"}, "*");

Objetos Proxy en JavaScript

El objeto Proxy de JavaScript es una potente funcionalidad introducida en ECMAScript 2015 (ES6) que permite crear un proxy para otro objeto. Esto permite interceptar y redefinir operaciones fundamentales de ese objeto, como la búsqueda de propiedades, la asignación, la enumeración y la invocación de funciones. Esta capacidad resulta especialmente útil en diversos escenarios avanzados, como la virtualización de objetos, el registro, el perfilado y la validación de datos.

Fundamentos de la instrumentación con objetos Proxy

Un objeto Proxy se crea con dos parámetros: el objeto de destino que encapsula y un objeto manejador (handler) que define las trampas (traps) para diversas operaciones. En el objeto manejador es donde se define la lógica de interceptación. Este es un ejemplo básico:

let target = {};
let handler = {
  get: function(obj, prop) {
    console.log(`Accessing property ${prop}`);
    return prop in obj ? obj[prop] : 37; // Default value
  }
};

let proxy = new Proxy(target, handler);
console.log(proxy.a); // Output: Accessing property a
                      // 37 (since 'a' is not a property of target)

En este ejemplo, la trampa get registra el acceso a las propiedades y devuelve un valor predeterminado si la propiedad no existe en el objeto de destino.

Limitaciones de los objetos Proxy

Aunque los Proxy de JavaScript ofrecen capacidades potentes para la instrumentación de seguridad y otras operaciones avanzadas, también presentan ciertas limitaciones. Comprender estas limitaciones es fundamental para que los desarrolladores utilicen los Proxy de forma eficaz en sus aplicaciones.

  1. Imposibilidad de interceptar acciones sobre tipos nativos: los Proxy de JavaScript no pueden interceptar directamente las acciones realizadas sobre tipos nativos como cadenas, números o booleanos. Dado que estos tipos no son objetos, un Proxy no puede interceptar las operaciones sobre ellos. Por ejemplo, no es posible usar un Proxy para interceptar directamente comparaciones o transformaciones de cadenas. Esta limitación puede afectar a la capacidad de supervisar y validar operaciones que involucran valores primitivos.
let proxy = new Proxy('example string', handler); // This will throw an error
  1. Incapacidad de supervisar algunas operaciones de objetos integrados: los Proxy no pueden interceptar ciertas operaciones sobre objetos integrados, como cambiar directamente la longitud de un array o establecer propiedades en funciones. Aunque es posible interceptar las llamadas a métodos y los accesos a propiedades, los cambios en propiedades internas que no activan el acceso mediante setter/getter quedan fuera del alcance de un Proxy.
let numbers = new Proxy([1, 2, 3], handler);

numbers.length = 2; // This operation cannot be intercepted directly
  1. No transparencia con algunas funciones integradas: ciertas funciones y métodos integrados de JavaScript pueden comportarse de forma diferente cuando sus argumentos son objetos Proxy. Por ejemplo, Array.isArray(proxyObject) devolverá false aunque el proxy encapsule un array. Este comportamiento no transparente puede producir resultados inesperados en el código que depende de la comprobación de tipos o de comportamientos nativos.

Detección de XSS en PostMessage con instrumentación mediante objetos Proxy

Perfilado de mensajes

Incorporar una fase de perfilado antes del fuzzing mejora la eficacia y la eficiencia de la identificación de vulnerabilidades en una aplicación JavaScript, en particular al centrarse en los manejadores de postMessage y en los patrones de acceso al almacenamiento.

El paso de perfilado tiene como objetivo reunir información detallada sobre el comportamiento de la aplicación, centrándose en capturar los manejadores y los mensajes de postMessage, así como las interacciones con el almacenamiento web (como localStorage y sessionStorage). Esta información cumple dos propósitos principales:

  • Reorientación del fuzzing: al recopilar información detallada sobre las interacciones y los flujos de datos de la aplicación, es posible adaptar las entradas del fuzzing para ejercitar más rutas de código de forma más eficaz, lo que permite descubrir vulnerabilidades que de otro modo podrían permanecer ocultas.
  • Mejora del rendimiento: el perfilado identifica qué entradas son relevantes y utiliza la aplicación, lo que permite que la fase de fuzzing posterior se concentre en esas áreas. Este enfoque dirigido evita perder tiempo en entradas irrelevantes y maximiza así la eficiencia del proceso de fuzzing.

Para capturar los manejadores de postMessage, se sobrescribe el método addEventListener con el fin de registrar todos los listeners de eventos que se registran. Esto es especialmente útil para identificar manejadores de mensajes que podrían ser objetivos potenciales del fuzzing.

// Object to store event listeners
const registeredEventListeners = [];


// Save the original addEventListener method
const originalAddEventListener = EventTarget.prototype.addEventListener;


// Override the addEventListener method
EventTarget.prototype.addEventListener = function(type, listener, options) {
   // Store the event details
   registeredEventListeners.push({ element: this, type, listener, options });


   // Call the original addEventListener method
   originalAddEventListener.call(this, type, listener, options);
};

Este fragmento de código almacena cada listener de eventos registrado en un array, lo que facilita revisar y analizar más adelante los listeners de eventos, en especial los asociados a eventos de mensaje.

Para supervisar y registrar la actividad de postMessage, se añade un manejador personalizado que registra los mensajes enviados, utilizando la consola por eficiencia y simplicidad.

function handleMessage(event) {
   console.error("magic_post_message", JSON.stringify({data: event.data, origin: event.origin}));
}

window.addEventListener('message', handleMessage, false);

Este manejador captura los mensajes entrantes y registra su contenido y su origen, lo que aporta información valiosa sobre cómo se utiliza postMessage dentro de la aplicación y destaca posibles áreas que investigar más a fondo durante el fuzzing.

El perfilado se extiende al seguimiento de cómo interactúa la aplicación con los objetos, incluidos los accesos a propiedades anidadas, mediante un Proxy que registra los accesos:

/**
* This function creates a proxy around the provided obj that tracks access to its properties. If a property of the object
* (or a nested object) is accessed, the accessHandler function is called with the object, the property name, and the path
* to the property.
*/
function createAccessTrackingProxy(obj, accessHandler) {
   // A recursive function to create a proxy for an object and its nested objects
   const createProxy = (target, path) => {
       return new Proxy(target, {
           get(target, property, receiver) {
               // Trigger the access handler function
               accessHandler(obj, property, path);


               // Check if the property accessed is an object and not null for recursion
               if (target[property] !== null && typeof target[property] === 'object') {
                   return createProxy(target[property], path.concat(property));
               }


               // Return the actual property value
               const value = Reflect.get(target, property, receiver);
               if (isObject(value)) {
                   return createProxy(value, path.concat(property));
               } else {
                   if (coinFlip()) {
                       return createProxy({}, path.concat(property));
                   } else {
                       return value;
                   }
               }


           }
       });
   };


   return createProxy(obj, []);
}


/**
* Logs path of object accesses
*/
function accessHandler(obj, property, path) {
   try {
       console.log('magic_post_message', JSON.stringify({
           data: obj,
           origin: null,
           path: [...path, property],
       }));
   } catch (e) {
       // Pass.
   }
}

Este enfoque consiste en crear un Proxy alrededor de un objeto (o de objetos anidados) y registrar cada acceso a sus propiedades. La función accessHandler se invoca cada vez que se accede a una propiedad y registra la ruta hasta la propiedad a la que se ha accedido. Este seguimiento detallado ayuda a comprender cómo interactúa la aplicación con sus datos y, a su vez, informa el proceso de fuzzing.

El uso de un lanzamiento de moneda en este contexto es una forma sencilla de introducir variabilidad en la fase de perfilado, ya que puede revelar rutas y comportamientos ocultos al decidir de forma dinámica si se devuelve el valor real o un proxy de un nuevo objeto.

Fuzzing dinámico de PostMessage

El paso de fuzzing consiste en enriquecer el corpus de mensajes recopilados durante la fase de perfilado con las rutas de acceso descubiertas. Este proceso de aumento es clave para descubrir vulnerabilidades más intrincadas, ya que garantiza que el fuzzing ejercite una gama más amplia de rutas de código dentro de la aplicación JavaScript. Tras el aumento, el conjunto de mensajes se minimiza para eliminar entradas redundantes o irrelevantes, lo que optimiza la eficiencia del proceso de fuzzing.

Ostorlab emplea un generador de puntos de inserción que comprende esquemas de codificación anidados. Esta capacidad es fundamental para probar aplicaciones que manejan objetos complejos, como los mensajes SAML, que pueden involucrar estructuras anidadas con codificaciones base64, XML, consultas HTTP y JSON.

El código proporcionado muestra cómo el fuzzer itera sobre los eventos postMessage recopilados, construye dinámicamente mensajes de fuzzing a partir de las rutas de acceso recopiladas y genera inserciones para sondear la aplicación:

if request.profile is not None:
    post_message_events = request.profile.post_messages or []
    for post_message_event in post_message_events:
        post_message = post_message_event["data"]

        # Traverse the message structure based on collected paths
        post_message_pointer = post_message
        for elm in post_message_event.get("path", []):
            if elm not in post_message_pointer:
                post_message_pointer[elm] = {}
            post_message_pointer = post_message_pointer[elm]

        # Generate and yield fuzzed messages
        for generated_insertions in self.generate(post_message):
            insertions = [WhatInsertionPoints.POST_MESSAGE, post_message]
            insertions.extend(generated_insertions)
            yield insertions

        # Repeat the process for JSON-encoded messages
        for generated_insertions in self.generate(json.dumps(post_message)):
            insertions = [WhatInsertionPoints.POST_MESSAGE, json.dumps(post_message)]
            insertions.extend(generated_insertions)
            yield insertions

La detección de vulnerabilidades de cross-site scripting (XSS) se produce cuando un callback de un método de JavaScript, activado por un mensaje de fuzzing, ejecuta código malicioso. Ostorlab mejora el proceso de detección utilizando console.trace para recopilar trazas de pila cada vez que se activa una vulnerabilidad XSS. Este enfoque permite identificar con precisión la ruta de ejecución que lleva a la vulnerabilidad, lo que facilita un análisis más profundo y una corrección más eficaz.

Conclusión

La combinación de perfilado, instrumentación con objetos Proxy y fuzzing dinámico tradicional ha demostrado ser eficaz para detectar vulnerabilidades XSS en PostMessage que antes pasaban inadvertidas.

Sin embargo, desafíos como la comparación de cadenas ponen de relieve la necesidad de nuevas mejoras. Nuestro siguiente paso es incorporar el análisis estático para detectar comparaciones de campos y lograr una cobertura aún mejor.

Etiquetas:

xss, instrumentation