In this article
Cómo prevenir vulnerabilidades de contaminación de prototipos en JavaScript
La contaminación de prototipos es una vulnerabilidad de JavaScript que permite a los atacantes agregar propiedades potencialmente maliciosas a los prototipos de objetos. Cuando los objetos definidos por el usuario heredan estos prototipos, se habilitan distintas formas de ataques posteriores.
En este artículo, aprenderás más sobre la contaminación de prototipos, cómo se produce, qué tipos de ataques puede facilitar y cómo prevenirlos. También aprenderás cómo las herramientas de Snyk pueden ayudarte a detectar y corregir esta vulnerabilidad en tu código y dependencias.
Qué son los prototipos en JavaScript
JavaScript usa algo llamado herencia prototípica. Un prototipo es un objeto con un conjunto de propiedades y funciones que se comparten entre todas las variables del mismo tipo. Cuando declaras una variable en JavaScript, esta accede automáticamente a uno o más prototipos.
Por ejemplo, cuando defines una variable numérica const one = 1, puedes acceder a la función toExponential() del prototipo de número. El prototipo de número también tiene su propio prototipo: el prototipo de objeto. Por eso, incluso cuando trabajas con un número, tienes acceso a hasOwnProperty(): una función definida en el prototipo de objeto que el prototipo de número no ha sobrescrito.
Cuando una variable numérica tiene un prototipo que, a su vez, se vincula con otro prototipo, se habla de una cadena de prototipos. Las cadenas de prototipos de todos los tipos terminan vinculándose al prototipo de objeto.
En la mayoría de los demás lenguajes, las relaciones de herencia se fijan en tiempo de compilación, pero en JavaScript se pueden actualizar en tiempo de ejecución.
Supongamos que agregas una propiedad o función al prototipo de cualquier variable. En ese caso, esa propiedad o función queda disponible para todas las demás variables del mismo tipo en la aplicación: tanto las creadas antes de la actualización como las creadas después.
Si agregas una propiedad o función al prototipo de objeto que está en la cima de la cadena de herencia, quedará disponible para todas las demás variables, a menos que la sobrescriban.
Por ejemplo, este es un objeto JavaScript sencillo:
const myFirstObject = {};
Para saber cuál es el prototipo de este objeto, puedes usar la palabra clave __proto__:
myFirstObject.__proto__Puedes usar el siguiente código para crear otro objeto vacío y luego agregar una propiedad a su prototipo:
const mySecondObject = {}
mySecondObject.__proto__.customProperty = "this is a property in the prototype"¿Qué pasa si registras los prototipos de estos dos objetos? Mira el siguiente código:
console.log(myFirstObject.__proto__)
console.log(mySecondObject.__proto__)El resultado de ambas llamadas a console.log() es idéntico:
[Object: null prototype] { customProperty: 'this is a property in the prototype' }
[Object: null prototype] { customProperty: 'this is a property in the prototype' }La customProperty que agregaste al prototipo del segundo objeto también aparece en el prototipo del primero. Ahora se comparte entre todos los objetos de tu aplicación.
Si agregas un tercer objeto vacío como este y quieres ver su prototipo, usa este código:
const myThirdObject = {}
console.log(myThirdObject.__proto__)También encontrarás ahí customProperty:
[Object: null prototype] { customProperty: 'this is a property in the prototype' }Si defines una propiedad propia en un objeto, se usará esa propiedad:
const nonEmptyObject = {
customProperty: "this is a property defined on the object itself"
}
console.log(nonEmptyObject.customProperty) // "this is a property defined on the object itself"Sin embargo, si usas un objeto con una propiedad que no está definida en él, el entorno de ejecución de JavaScript buscará la propiedad en la cadena de prototipos del objeto y usará la que encuentre:
const anotherEmptyObject = {}
console.log(anotherEmptyObject.customProperty) // "this is a property in the prototype"Qué es la contaminación de prototipos en JavaScript
La contaminación de prototipos es una vulnerabilidad que permite a los atacantes inyectar propiedades y funciones maliciosas en los prototipos de las aplicaciones JavaScript. Sin embargo, la contaminación de prototipos no es un ataque en sí, sino que facilita distintos ataques. El prototipo de objeto es el objetivo más común de estos ataques, ya que infectarlo con código malicioso permite al atacante llegar al mayor número posible de objetos.
Tanto el código JavaScript del lado del servidor como el del lado del cliente pueden ser vulnerables a ataques de contaminación de prototipos. En el servidor, esta vulnerabilidad suele facilitar la escalada de privilegios, la denegación de servicio o la ejecución remota de código. En el cliente, puede abrir la puerta a DOM XSS.
Esta vulnerabilidad puede presentarse en aplicaciones escritas en JavaScript y en cualquier lenguaje que se compile a JavaScript, incluido, sobre todo, TypeScript. Además, la contaminación de prototipos puede aparecer en tu código y tus dependencias, incluidas algunas versiones de bibliotecas populares como Lodash, collection.js y jQuery.
Imagina que un atacante logra definir la siguiente propiedad en el prototipo de objeto de tu aplicación JavaScript en ejecución:
isAdmin: true
Si puede hacerlo, el atacante puede buscar en el código de tu aplicación comprobaciones de nivel de acceso con poca protección. Si una comprobación depende de un objeto JavaScript que representa al usuario actual y ese objeto no tiene la propiedad isAdmin: false, el entorno de ejecución encontrará isAdmin: true en el prototipo. A partir de ahí, al atacante le resulta fácil escalar privilegios y acceder a áreas restringidas de tu aplicación.
Sin embargo, no se trata solo de definir nuevas propiedades en un prototipo. Un atacante también puede sobrescribir funciones que ya existen en él. Por ejemplo, supongamos que un atacante logra sobrescribir la función toString() del prototipo de objeto con una llamada recursiva:
let newObject = {}
newObject.__proto__.toString = function() {this.toString()}Después de esto, la primera llamada a toString() en un objeto, o incluso la interpolación de un objeto en una cadena de plantilla, hará que el proceso anfitrión se detenga con un RangeError: Maximum call stack size exceeded error.
Una forma común en que los atacantes contaminan un prototipo es mediante implementaciones inseguras de combinación recursiva de propiedades en el código de las aplicaciones y en las bibliotecas.
La contaminación de prototipos en acción
Experimentemos con una aplicación de ejemplo para entender cómo funciona la contaminación de prototipos en Node.js. Trabajarás con una aplicación Express, una base de datos MongoDB en memoria y algunos endpoints.
Clona este repositorio si quieres seguir los pasos. Una vez que lo hagas, restaura las dependencias e inicia la aplicación:
npm install
npm startAbre http://localhost:8080/todos/ en tu navegador o envía una solicitud GET a http://localhost:8080/todos/ desde tu cliente HTTP favorito. Recibirás un arreglo con tres tareas pendientes:
[
{
"_id": "654c062468efa31d8c055c13",
"text": "Jason's first todo item",
"open": true,
"visible": true,
"owner": "654c062468efa31d8c055c0e",
"__v": 0
},
{
"_id": "654c062468efa31d8c055c14",
"text": "First todo for Kelly",
"open": true,
"visible": true,
"owner": "654c062468efa31d8c055c0f",
"__v": 0
},
{
"_id": "654c062468efa31d8c055c15",
"text": "Paul's thing to be done",
"open": true,
"visible": true,
"owner": "654c062468efa31d8c055c10",
"__v": 0
}
]
Ahora, si abres database/seed.js en el repositorio clonado, verás que la base de datos de la aplicación se inicializó con cuatro tareas pendientes, no tres:
const seedTodoItems = [
{
text: "Jason's first todo item",
open: true,
visible: true,
owner: "Jason"
},
{
text: "First todo for Kelly",
open: true,
visible: true,
owner: "Kelly"
},
{
text: "Paul's thing to be done",
open: true,
visible: true,
owner: "Paul"
},
{
text: "A HIDDEN TODO",
open: true,
owner: "Alexandra"
}
]
Tres de las tareas iniciales se marcaron explícitamente como visibles, pero una no, lo que significa que está oculta de forma predeterminada. Si abres server.js y revisas el endpoint que atiende la solicitud GET, verás que solo devuelve las tareas visibles:
app.get('/todos/', (req, res) => {
TodoItem.find({visible: true})
.then(data => res.json(data))
.catch(error => res.json({error}))
})Usa otro endpoint para agregar una nueva tarea pendiente. Envía la siguiente solicitud POST a http://localhost:8080/todos/add:
POST http://localhost:8080/todos/add
Content-Type: application/json
{
"__proto__": {
"visible": true
},
"text": "😈 new todo item that tries to mess with the prototype",
"owner": "654c0cc960d82b5802a0b30d"
}Puedes usar la siguiente solicitud curl desde la línea de comandos para crear una nueva tarea pendiente:
curl -X POST -H 'content-type: application/json' http://localhost:8080/todos/ad
d --data '{ "__proto__": {
"visible": true
},
"text": "😈 new todo item that tries to mess with the prototype",
"owner": "654c0cc960d82b5802a0b30d"
}'Ahora vuelve a enviar una solicitud GET a http://localhost:8080/todos/. Esto es lo que recibirás:
[
{
"_id": "654c0d59654aeeb63dfd5ae8",
"text": "Jason's first todo item",
"open": true,
"visible": true,
"owner": "654c0d59654aeeb63dfd5ae3",
"__v": 0
},
{
"_id": "654c0d59654aeeb63dfd5ae9",
"text": "First todo for Kelly",
"open": true,
"visible": true,
"owner": "654c0d59654aeeb63dfd5ae4",
"__v": 0
},
{
"_id": "654c0d59654aeeb63dfd5aea",
"text": "Paul's thing to be done",
"open": true,
"visible": true,
"owner": "654c0d59654aeeb63dfd5ae5",
"__v": 0
},
{
"_id": "654c0d59654aeeb63dfd5aeb",
"text": "A HIDDEN TODO",
"open": true,
"owner": "654c0d59654aeeb63dfd5ae6",
"__v": 0
},
{
"_id": "654c0e6f654aeeb63dfd5aef",
"text": "😈 new todo item that tries to mess with the prototype",
"open": true,
"owner": "654c0cc960d82b5802a0b30d",
"__v": 0
}
]
Al principio tenías tres tareas pendientes visibles. Agregaste una más. Pero ahora hay cinco en lugar de cuatro. ¡La tarea pendiente que debía estar oculta ahora está visible!
Veamos qué ocurre en el endpoint que agrega una nueva tarea pendiente:
app.post('/todos/add', (req, res) => {
const defaults = {
open: true,
}
const todoToAdd = lodash.merge(defaults, req.body)
const todoItem = new TodoItem(todoToAdd);
todoItem.save()
.then(() => res.json({msg: `Successfully added a todo item`}))
.catch(error => res.json({error: error.message}))
})
La aplicación analiza el cuerpo de la solicitud POST (un objeto JSON) y lo combina con el objeto defaults, que se usa para cada nueva tarea pendiente. Para realizar la combinación, usa el método merge() de una versión vulnerable de Lodash. Como resultado, la parte maliciosa de la solicitud que manipula el prototipo de objeto se combina sin problemas con el objeto resultante y, en el proceso, establece Object.__proto__.visible en true.
Como resultado, todos los objetos existentes y futuros que no declaren explícitamente una propiedad visible empiezan a obtenerla de su prototipo, que sí la tiene. Cuando la aplicación consulta la base de datos al procesar una solicitud GET, usa un filtro diseñado para recuperar solo las notas de tareas pendientes visibles:
app.get('/todos/', (req, res) => {
TodoItem.find({visible: true})
.then(data => res.json(data))
.catch(error => res.json({error}))
})
Al evaluar el filtro, resulta que la tarea pendiente oculta no tiene su propia propiedad visible: false:
{
text: "A HIDDEN TODO",
open: true,
owner: "Alexandra"
}Sin embargo, ahora el prototipo tiene establecido visible: true. Se usa el valor de la propiedad del prototipo, lo que provoca una filtración accidental de una tarea pendiente existente que debía permanecer oculta. Además, se revela la tarea pendiente agregada como parte de la solicitud maliciosa, aunque no tenga establecido visible: true.
Cómo prevenir las vulnerabilidades de contaminación de prototipos
En la aplicación de ejemplo anterior, corregir la vulnerabilidad es tan fácil como actualizar Lodash a una versión más reciente. Sin embargo, hay otras formas de evitar la contaminación de prototipos que debes conocer, sobre todo cuando revisas tu propio código y no el de una biblioteca.
Sanitiza las claves de las propiedades
La forma más evidente de abordar el problema es comprobar si una clave es segura antes de combinarla:
if (key == '__proto__') {
return;
}Sin embargo, mantener una lista de denegación no es lo ideal, ya que se conocen formas de eludir esta protección.
Una mejor opción es mantener una lista de permitidos con las claves de propiedades autorizadas:
if (allowedKeys.includes(key)) {
// Proceed with merge
}Usa Map en lugar de un objeto
JavaScript ofrece un objeto alternativo para almacenar pares clave-valor: Map. Aunque es similar a un objeto en muchos aspectos, hay una diferencia importante: cuando usas la función get() de un mapa, solo puedes obtener lo que agregaste explícitamente al mapa. No se accederá a los elementos maliciosos que pudieran haberse agregado al prototipo de objeto:
const defaultsMap = new Map()
defaultsMap.set("open", true);
defaultsMap.set("hidden", false)
// vs
const defaultsObject = {
open: true,
hidden: false
}Usa Object.freeze() o Object.seal() para impedir cambios
JavaScript ofrece dos funciones integradas que limitan los cambios que se pueden hacer en los objetos: Object.freeze() y Object.seal(). Como el prototipo de objeto también es un objeto, puedes usar estas funciones con él:
Object.freeze(Object.prototype)Para prevenir la contaminación de prototipos, se recomienda Object.freeze(), porque prohíbe tanto agregar propiedades nuevas como modificar las existentes en un objeto.
Object.seal() impide agregar propiedades nuevas, pero permite actualizar las existentes. Por eso, si usas Object.seal(), todavía es posible contaminar el prototipo sobrescribiendo sus funciones, como toString().
Usa el paquete nopp de Snyk
Una buena extensión del concepto de congelar objetos es el paquete nopp de Snyk. Aplica Object.freeze() a algunos de los objetos integrados de JavaScript. Si lo usas hacia el final de la inicialización de tu aplicación, permite cambios legítimos en los prototipos, pero bloquea los intentos maliciosos de contaminación cuando la aplicación ya está en pleno funcionamiento.
En la aplicación Express de ejemplo que usamos antes, agregar nopp es tan sencillo como instalarlo:
npm install noppY agregarlo como última instrucción de importación en server.js:
import express from 'express';
import connect from "./database/connection.js";
import seedDatabase from "./database/seed.js";
import TodoItem from "./model/todoitem.model.js";
import lodash from "lodash";
import cors from "cors";
import "nopp";En cuanto agregas nopp, se mitiga el escenario de contaminación de prototipos que permitiría escalar privilegios, aunque la aplicación de ejemplo siga usando una versión vulnerable de Lodash.
Usa la extensión de IDE de Snyk para detectar vulnerabilidades de contaminación de prototipos
Es importante tener en cuenta que las vulnerabilidades, incluida la contaminación de prototipos, son más fáciles de corregir si son fáciles de detectar. Contar con una herramienta que señale posibles problemas de seguridad en tu editor de código puede ayudarte mucho a escribir código JavaScript seguro.
Si te interesa una herramienta como esta, considera instalar la extensión Snyk Security para Visual Studio Code. (Si trabajas con IDE de JetBrains, como WebStorm, también hay una extensión para ti.)
La extensión de Snyk identifica y resalta posibles vulnerabilidades de contaminación de prototipos y otros tipos en tu código JavaScript y tus bibliotecas. Para cada vulnerabilidad detectada, muestra cómo distintos proyectos de código abierto corrigieron problemas similares.
A continuación, se muestra una captura de pantalla de la extensión de Snyk para el IDE de VS Code en acción. Detecta varias vulnerabilidades en el paquete npm lodash, como contaminación de prototipos, denegación de servicio mediante expresiones regulares e inyección de comandos:

Para instalar la extensión de Snyk, busca "snyk" en el panel de extensiones de Visual Studio Code:

Instala la extensión llamada "Snyk Security - Code, Open Source Dependencies, IaC Configurations":

La extensión de Snyk se instala junto con Snyk CLI, que necesitarás para ejecutar inspecciones de Snyk de ahora en adelante.
Ahora haz clic en el ícono de Snyk en la barra de menú izquierda de Visual Studio Code y, en el panel de Snyk, haz clic en Trust workspace and connect:

Snyk abre una ventana del navegador donde debes iniciar sesión en tu cuenta de Snyk o crear una:

Después de iniciar sesión, Snyk debe autenticar tu equipo para asociar tu Snyk CLI local con tu cuenta de Snyk:

Después de hacer clic en Authenticate, la aplicación web de Snyk verifica que te hayas autenticado correctamente:

Ahora puedes cerrar la ventana del navegador y volver a Visual Studio Code. Una vez allí, cuando abras un espacio de trabajo o una carpeta, Snyk iniciará el análisis de vulnerabilidades.
Así podría verse Visual Studio Code después de permitir que la extensión de Snyk analice un proyecto:

En el panel “SNYK” de la izquierda, verás una lista de problemas de seguridad y calidad del código identificados, como contaminación de prototipos, denegación de servicio mediante expresiones regulares (ReDoS), falsificación de solicitudes entre sitios (CSRF) y exposición de información.
En la pestaña “Editor”, las líneas de código afectadas aparecen subrayadas. Al presionar Ctrl +. (o Cmd + . en Mac), se muestra un menú con acciones rápidas relacionadas. En un panel aparte, la extensión de Snyk resume la vulnerabilidad detectada y muestra ejemplos de cómo se abordaron vulnerabilidades similares en varios proyectos de código abierto.
La flexibilidad de JavaScript tiene un costo, y la contaminación de prototipos es un excelente ejemplo de lo fácil que es abusar de JavaScript si los desarrolladores no toman las medidas adecuadas para proteger su código.
La mejor manera de aprender sobre seguridad de aplicaciones es programando. ¡No te pierdas la lección de Snyk Learn sobre contaminación de prototipos, que incluye ejemplos de código interactivos!

Las extensiones de seguridad de Snyk para IDE pueden ayudarte a detectar a tiempo la contaminación de prototipos y otras vulnerabilidades sin tener que salir de tu editor de código favorito.
Usar las partes más seguras de JavaScript y herramientas inteligentes para desarrolladores te ayuda a publicar código JavaScript seguro y evitar costosos incidentes de seguridad.
Empieza a proteger tus aplicaciones JavaScript
Encuentra y corrige vulnerabilidades de JavaScript gratis con Snyk.
No necesitas tarjeta de crédito.
O regístrate con Azure AD Docker ID Bitbucket
Al usar Snyk, aceptas cumplir nuestras políticas, incluidos nuestros Términos del servicio y nuestra Política de privacidad.