Skip to main content

Tras tres años de silencio, vuelve a surgir una nueva vulnerabilidad de contaminación de prototipos en jQuery

Escrito por
jQuery Blog

15 de abril de 2019

0 minutos de lectura

El 26 de marzo de 2019, casi tres años después de que se divulgara la última vulnerabilidad de seguridad de jQuery, nos enteramos recientemente de una nueva vulnerabilidad de seguridad que afecta a la misma popular biblioteca frontend de jQuery.

Esta vulnerabilidad de seguridad, denominada contaminación de prototipos, permite a los atacantes sobrescribir el prototipo de un objeto de una aplicación JavaScript. Cuando esto sucede, se pueden inyectar en los objetos propiedades controladas por el atacante, lo que puede provocar una denegación de servicio al generar excepciones de JavaScript o alterar el código fuente de la aplicación para forzar la ejecución de la ruta de código inyectada por el atacante.

A continuación se muestra un ejemplo de prueba de concepto del informe original que clasifiqué en HackerOne como parte del grupo de trabajo (WG) de seguridad de Node.js:

let a = $.extend(true, {}, JSON.parse('{"__proto__": {"devMode": true}}'))
console.log({}.devMode); // true

Como muestra el código, se usa la API extendida de jQuery para combinar varios objetos de forma recursiva.

Aunque no es una vulnerabilidad muy sencilla de explotar, podría afectar a una gran cantidad de proyectos y usuarios debido a la popularidad de jQuery en el ecosistema de JavaScript. Además, ya hemos visto casos reales de ataques de contaminación de prototipos, como el que afectó a mongoose en diciembre de 2018.

El equipo de jQuery lanzó recientemente una corrección para este problema de seguridad en la versión 3.4.0, a la que te recomendamos encarecidamente actualizar.

Reconstrucción de una aplicación vulnerable

Dado que jQuery es una biblioteca que se usa principalmente en el frontend, veamos cómo se manifiesta una vulnerabilidad de contaminación de prototipos en una aplicación del lado del cliente.

El ataque comienza con la entrada del usuario, que permite que un atacante malicioso inyecte un objeto que el desarrollador quizá no haya saneado ni haya considerado para un tratamiento especial.

Supongamos que estás creando una aplicación en la que un usuario tiene autorización para enviar cargas JSON que se guardan tal cual. Tal vez quieras darle esta capacidad para que pueda controlar la estructura del contenido, de la que no deseas hacerte responsable.

A continuación se muestra un ejemplo de una carga que se enviaría en ese caso:

{
  “myProperty” : “a”,
  "__proto__" : { "isAdmin" : true }
}

Supongamos que tienes la tarea de clonar un objeto de forma recursiva, pero no tienes del todo claro cómo hacerlo. Si buscas en Google "deep clone an object in javascript", esta respuesta aparece como el principal resultado de búsqueda de Stack Overflow:

Al ver que tiene más de cuatro mil votos positivos y fue elegida como la respuesta correcta, quizá tengas muchas ganas de copiar y pegar el ejemplo de copia profunda para terminar el trabajo.

var myObject = ‘{ “myProperty” : “a”, "__proto__" : { "isAdmin" : true } }’
var newObject = jQuery.extend(true, {}, JSON.parse(myObject))

Según el consejo de Stack Overflow, ¿qué esperarías que ocurriera con este ejemplo de código?

  1. Un myObject muestra un ejemplo en formato de cadena, posiblemente obtenido de un campo de una base de datos.

  2. Se usan JSON.parse() y la función extend() de jQuery para clonar myObject.

  3. La nueva versión clonada se llama newObject.

Cuando JSON.parse() procesa una propiedad llamada __proto__, como la de nuestro ejemplo, quizá esperarías que asignara la propiedad isAdmin a true en la propiedad principal del objeto. Sin embargo, en realidad crea un objeto con ese nombre de propiedad, lo que sobrescribe la capacidad de encadenamiento de propiedades.

Cuando este comportamiento de JSON.parse() se combina con una capacidad insegura de clonación profunda, también conocida como combinación de objetos, el valor asignado a la propiedad __proto__ se filtra al objeto global de JavaScript.

Con esto en mente, imaginemos el siguiente fragmento de código y su impacto para entender cómo funcionan los ataques de contaminación de prototipos:

var myObject = ‘{ “myProperty” : “a”, "__proto__" : { "isAdmin" : true } }’
var newObject = jQuery.extend(true, {}, JSON.parse(myObject))
// if you do console.log({}.isAdmin) you’ll get true returned
// later down the application source code we may try to detect if the user is an admin or not
If (user.isAdmin === true) {
  // do something like load the relevant interface, run an Ajax call, access localStorage, etc
}

Si el objeto de usuario obtenido de la base de datos no tuviera ningún valor asignado a su propiedad isAdmin, entonces esa propiedad del objeto de usuario no estaría definida. En ese caso, para acceder a la propiedad isAdmin en la cláusula if habría que acceder al objeto principal en la cadena de prototipos del objeto user. Ese objeto sería Object, que ahora está contaminado e incluye la propiedad isAdmin, con el valor true. En última instancia, sin que el desarrollador lo haya previsto, el usuario queda configurado como administrador y puede causar estragos en la aplicación.

Exploración de otros vectores de ataque

Como acabamos de ver en el ejemplo anterior, una operación insegura de combinación recursiva, junto con el funcionamiento de JSON.parse, podría contaminar la cadena de prototipos.

Sin embargo, esta no es la única forma de alterar el prototipo. Considera el siguiente código:

let myObj = {}
myObj[‘__proto__’][‘a’] = ‘a’
console.log(myObj.a)
let newObj = {}
console.log(newObj.a)

En este ejemplo:

  1. Se crea un nuevo myObj.

  2. Se accede a la cadena de prototipos a través de __proto__ y se modifica ese objeto para incluir una nueva propiedad de cadena.

Como el prototipo de myObj es en realidad un Object de JavaScript que modificamos, todos los objetos nuevos que se creen a partir de ahora también incluirán esta propiedad. Esto ocurre por el funcionamiento de JavaScript: si no hay ninguna propiedad en el objeto a (en newObj), JavaScript accede al prototipo de newObj para encontrarla y lo hace de forma recursiva hasta recorrer toda la cadena de prototipos. En nuestro caso, esa propiedad sí existe, por eso al acceder a ella se devuelve el valor de cadena.

Quizá te preguntes cómo podría alguien inyectar el acceso al objeto __proto__ de la forma que acabamos de describir.

Consideremos la creación de una aplicación que enumera paquetes de npm y realiza alguna acción con ellos, como mostrarlos en una interfaz de usuario atractiva.

Probablemente necesitaríamos una API que envíe una lista de paquetes de npm y el contenido de sus archivos package.json:

[
  {
    "cool-package": {
      "license": "MIT",
      "github": "https://github.com/someuser/cool-package"
    }
  },
  {
    "__proto__": {
      "license": "MIT",
      "github": "https://github.com/",
      “toString”: “april fools”
    }
  }
]

Quizá pienses que esta API es segura porque los datos llegan directamente desde npmjs y las API que ofrece, o desde otro sitio web de confianza que proporciona estos datos y cuyo servicio de API no controla el atacante. Pero eso sería un error. Como seguramente ya notaste, los detalles del paquete, como su nombre y el contenido de package.json, en última instancia están bajo el control del usuario.

Siguiendo con este escenario de aplicación, supongamos que el desarrollador quiere crear un mapa a partir del arreglo para acceder fácilmente a los paquetes sin tener que recorrer el arreglo para obtener los datos.

En esencia, el desarrollador podría intentar acceder a los datos de esta manera:

const pkgLicense = packageList[‘jquery’][‘license’]

Este es un ejemplo funcional de ese código:

let list_of_npm_packages = JSON.parse(`
[
  {
    "cool-package": {
      "license": "MIT",
      "github": "https://github.com/someuser/cool-package"
    }
  },
  {
    "__proto__": {
      "toString": "april fools"
    }
  }
]
`);

list_of_npm_packages.forEach((package) => {
     for (const [propertyKey, objectValue] of Object.entries(package)) {
          for (const [name, value] of Object.entries(objectValue)) {            
            if (!packagesMap[propertyKey]) {
            packagesMap[propertyKey] = {}
            }
            packagesMap[propertyKey][name] = value 
          }
     }
})

Si en este punto creáramos objetos completamente nuevos e intentáramos acceder a su propiedad toString, descubriríamos el ataque de contaminación de prototipos.

const a = {}
console.log(a.toString)  // prints ‘april fools’
console.log({}.toString) // prints ‘april fools’

Vulnerabilidades de contaminación de prototipos en el mundo real

jQuery no es la única víctima de este tipo de vulnerabilidad. Tan solo el último año, registramos más de 20 vulnerabilidades de contaminación de prototipos en los ecosistemas de navegadores y Node.js. Estas vulnerabilidades aparecen tanto en bibliotecas de JavaScript como lodash, que podrían afectar a muchos proyectos frontend debido a la popularidad de lodash, como en bibliotecas backend de Node.js que manejan la clonación de objetos de JavaScript, como node.extend y deep-extend.

El 21 de febrero, Eran Hammer también compartió su experiencia sobre cómo un ataque similar, una vulnerabilidad de envenenamiento de prototipos, puede afectar a una biblioteca al filtrar datos a través de joi, un popular paquete de validación que impulsa muchos proyectos del ecosistema. Esta vulnerabilidad también afectó a hoek, una biblioteca de utilidades del mismo grupo de desarrolladores, dentro del framework de aplicaciones web hapi.

Olivier Arteau, también conocido como HoLyVieR, ha reportado muchas de estas vulnerabilidades de contaminación de prototipos mediante divulgaciones de seguridad responsables al programa de HackerOne que administra el grupo de trabajo de seguridad de Node.js, cuyo objetivo es ofrecer respuesta a incidentes y gestionar problemas de seguridad que afectan al ecosistema de JavaScript en general. Oliver también publicó un informe detallado sobre la vulnerabilidad, en el que analiza el impacto de la contaminación de prototipos, y presentó un caso real de esta vulnerabilidad que afectó al proyecto Ghost CMS de Node.js en la conferencia NorthSec.

Resumen

En conclusión, para evitar la contaminación de prototipos, debes aplicar varias medidas de mitigación y seguir las mejores prácticas de seguridad:

  • Asegúrate de usar implementaciones seguras de combinación recursiva.

  • Considera crear objetos sin prototipo, como Object.create(null), para evitar que sean susceptibles a ataques de contaminación de prototipos.

  • Evita usar la notación de corchetes al trabajar con datos controlados por el usuario y, si es posible, evita usarla por completo. Considera usar la primitiva del lenguaje Map para estructuras basadas en mapas.

En Snyk valoramos a la comunidad de seguridad y creemos que la divulgación responsable de vulnerabilidades de seguridad en paquetes de código abierto nos ayuda a garantizar la seguridad y la privacidad de los usuarios. Si crees que encontraste una vulnerabilidad en un paquete de código abierto, puedes contactarnos en https://snyk.io/vulnerability-disclosure. Nuestro programa de divulgación responsable busca proteger tanto al desarrollador como al investigador que reporta la vulnerabilidad, y permite que los desarrolladores se beneficien de forma segura de las vulnerabilidades que descubren los investigadores.

Empieza con los desafíos de Capture the Flag

Aprende a resolver desafíos de captura la bandera con nuestro taller virtual introductorio a pedido.