Prototype pollution (contaminación de prototipos) es una vulnerabilidad propia de JavaScript en la que un atacante inyecta propiedades dentro del prototipo del que heredan todos los objetos de la aplicación, normalmente Object.prototype. Como cualquier objeto plano de JavaScript resuelve sus propiedades subiendo por la cadena de prototipos, alterar ese prototipo base afecta de forma global a estructuras de datos que el desarrollador nunca manipuló de manera explícita. Según los gadgets presentes en el código, esa contaminación puede escalar hasta ejecución remota de código (RCE), denegación de servicio (DoS) o bypass de autenticación y autorización.
Lo esencial. Prototype pollution no explota un fallo de memoria ni roba credenciales: abusa del modelo de herencia prototípica de JavaScript. El atacante controla una clave especial (
__proto__,constructoroprototype) que un sink inseguro (merge recursivo, clonado profundo, parser de query string) termina escribiendo sobre el prototipo compartido. La defensa combina objetos sin prototipo (Object.create(null)), congelarObject.prototype, validación estricta de claves y el flag--disable-protode Node.js.
Qué es prototype pollution: la cadena de prototipos
En JavaScript casi todo objeto plano hereda de Object.prototype. Cuando el motor busca una propiedad que no existe como propiedad propia (own property), sube por la cadena de prototipos hasta encontrarla o agotar la cadena. Esa herencia es la que permite que ({}).toString o ({}).hasOwnProperty funcionen sin haberlos definido.
El problema aparece porque tres claves tienen un significado especial. El accesor __proto__ apunta directamente al prototipo del objeto. constructor apunta a la función constructora, y constructor.prototype vuelve a llevar al prototipo compartido. Si un atacante consigue que la aplicación escriba en obj.__proto__.algo o en obj.constructor.prototype.algo, no está modificando ese objeto concreto: está modificando el prototipo del que heredan todos los demás objetos del proceso.
El resultado es que, tras la contaminación, cualquier objeto de la aplicación que consulte una propiedad inexistente recibirá el valor inyectado por el atacante. Una comprobación tan inocente como config.isAdmin puede devolver true sin que nadie haya escrito nunca esa propiedad en config.
Cómo se produce: el sink vulnerable
Merge recursivo inseguro
El patrón que causa la mayoría de casos reales es una función de mezcla profunda que copia claves de un objeto controlado por el usuario a un objeto de destino sin filtrar claves peligrosas:
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object' && source[key] !== null) {
if (!target[key]) target[key] = {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
Cuando key vale __proto__, la expresión target['__proto__'] no crea una propiedad nueva: devuelve el propio Object.prototype, y la recursión escribe la carga útil directamente sobre él. Este es el fallo que afectó a versiones antiguas de lodash (_.merge, _.defaultsDeep), de jQuery ($.extend(true, ...)) y de decenas de utilidades de configuración.
Vectores de entrada
El JSON de una petición es el vector más directo. Un cuerpo como el siguiente, pasado a un merge vulnerable, escribe isAdmin en el prototipo global:
{ "__proto__": { "isAdmin": true } }
Las query strings son igual de peligrosas cuando el parser expande corchetes en objetos anidados (el comportamiento por defecto de librerías como qs):
POST /api/update?__proto__[isAdmin]=true
POST /api/update?constructor[prototype][role]=admin
Conviene aclarar un matiz técnico frecuente: JSON.parse('{"__proto__":{}}') crea __proto__ como propiedad propia y no contamina el prototipo por sí solo. La contaminación se produce después, cuando un merge o un clonado copian esa clave con notación de corchetes hacia el prototipo compartido. Por eso el sink importa tanto como la entrada.
Del prototipo contaminado al impacto
Bypass de autenticación y autorización
Muchos controles de acceso comprueban banderas booleanas por herencia sin verificar que sean propiedades propias. Si el atacante contamina Object.prototype.isAdmin o Object.prototype.role, cualquier objeto de sesión que no defina esas claves las heredará contaminadas:
if (user.isAdmin) { /* concede acceso administrativo */ }
Con el prototipo contaminado, user.isAdmin devuelve true aunque user nunca tuvo esa propiedad. El resultado es una escalada de privilegios que comparte raíz conceptual con los fallos de control de acceso descritos en Qué es IDOR: broken access control.
Denegación de servicio (DoS)
Contaminar propiedades que el motor o el framework consultan internamente puede romper la aplicación entera. Inyectar un valor en Object.prototype.toString, o saturar el manejo de rutas de un framework, provoca excepciones no controladas en peticiones legítimas posteriores. La vulnerabilidad CVE-2022-24999, en el paquete qs usado por Express, permitía precisamente degradar el servicio manipulando el prototipo desde la query string.
Ejecución remota de código (RCE)
El escenario más grave llega cuando existe un gadget que convierte una propiedad heredada en ejecución. La investigación "Silent Spring: Prototype Pollution Leads to Remote Code Execution in Node.js" (Shcherbakov et al., USENIX Security 2023) documentó cadenas en las que contaminar opciones de child_process (por ejemplo shell, env o NODE_OPTIONS con --require) fuerza a Node.js a cargar y ejecutar código del atacante. El caso público más conocido es CVE-2019-7609 en Kibana, donde una contaminación de prototipos en el componente Timelion se encadenaba hasta RCE en el servidor.
Prototype pollution en cliente y en servidor
En el navegador, la contaminación de prototipos rara vez es el objetivo final: actúa como paso previo para un DOM XSS. Muchas librerías cliente construyen elementos o URLs a partir de propiedades que, si están contaminadas, se convierten en gadgets de inyección. La extensión DOM Invader de Burp Suite automatiza la detección de estos gadgets del lado cliente.
En el servidor (server-side prototype pollution o SSPP), el impacto es mayor porque los gadgets disponibles en Node.js incluyen ejecución de procesos, carga de módulos y manejo de plantillas. Un SSPP explotable suele significar RCE, no solo manipulación de la interfaz. Este tipo de fallo encaja de lleno en el trabajo descrito en pentesting de APIs REST y GraphQL, donde los cuerpos JSON son la superficie de entrada natural.
CVEs y casos reales
Prototype pollution acumula un historial extenso en el ecosistema npm. Algunos ejemplos representativos: lodash (CVE-2019-10744, en defaultsDeep), jQuery (CVE-2019-11358, en $.extend, corregido en 3.4.0), el parser ini (CVE-2020-7788), tough-cookie (CVE-2023-26136) y el ya citado Kibana (CVE-2019-7609). El patrón común es siempre el mismo: una utilidad ampliamente usada que copia claves controladas por el usuario sin bloquear __proto__ ni constructor. Por su carácter transitivo, una sola dependencia vulnerable en el árbol de node_modules basta para exponer a toda la aplicación, algo que conecta con los riesgos de la cadena de suministro de software.
Defensa y hardening
A nivel de código
La medida más efectiva es no usar objetos planos como diccionarios de datos no confiables. Object.create(null) crea objetos sin prototipo, inmunes a la contaminación por herencia, y Map evita por completo el problema para pares clave-valor dinámicos. Cuando el merge sea inevitable, hay que rechazar explícitamente las claves peligrosas:
const BLOCKED = ['__proto__', 'constructor', 'prototype'];
for (const key in source) {
if (BLOCKED.includes(key)) continue;
// ... copia segura
}
Congelar el prototipo al arrancar el proceso, con Object.freeze(Object.prototype), impide cualquier escritura posterior sobre él, aunque conviene probarlo porque algunas librerías legacy escriben en el prototipo de forma legítima.
A nivel de plataforma Node.js
Node.js ofrece el flag --disable-proto=throw (o --disable-proto=delete) que neutraliza el accesor __proto__, cerrando el vector más común. El flag experimental --frozen-intrinsics va más allá y congela los objetos internos del motor. Ambos deben validarse en un entorno de staging antes de producción.
A nivel de librerías y dependencias
Validar toda entrada contra un esquema estricto con Ajv y additionalProperties: false descarta claves inesperadas antes de que lleguen a ningún merge. Para parsear JSON no confiable, secure-json-parse elimina las claves peligrosas. Y mantener las dependencias actualizadas (lodash 4.17.21 o superior, jQuery 3.4.0 o superior) mediante npm audit, Snyk o Socket cierra los CVE conocidos.
Cómo detectarlo en una auditoría
La detección combina análisis estático, dinámico y manual. La consulta js/prototype-pollution de CodeQL y las reglas de Semgrep identifican merges recursivos inseguros en el código fuente. En dinámico, DOM Invader (Burp Suite) cubre el lado cliente, y herramientas como proto-find, ppfuzz o ppmap ayudan a fuzzear parámetros en el servidor. Los laboratorios de PortSwigger Web Security Academy sobre prototype pollution son la referencia práctica para reproducir cadenas cliente y servidor en un entorno controlado.
La confirmación manual sigue siendo imprescindible: enviar {"__proto__":{"secraTest":"x"}} a un endpoint y comprobar después, en otra petición, si un objeto no relacionado hereda secraTest es la prueba directa de que existe un sink explotable. A partir de ahí, el trabajo consiste en localizar el gadget que convierte esa contaminación en impacto real, un proceso análogo al de otras clases de inyección como la inyección SQL o la inyección de comandos.
Preguntas frecuentes
¿Prototype pollution solo afecta a Node.js?
No. Es un problema del lenguaje JavaScript, así que afecta tanto al backend (Node.js, Deno, Bun) como al navegador. En el servidor suele derivar en RCE o DoS, mientras que en el cliente actúa como gadget previo a un DOM XSS. El vector cambia, pero la raíz (herencia prototípica y sinks inseguros) es la misma.
¿Basta con bloquear la clave proto?
No es suficiente por sí solo. Un filtro que solo rechace __proto__ deja abierto el vector constructor.prototype, que llega al mismo prototipo compartido por otra ruta. La lista de bloqueo debe incluir __proto__, constructor y prototype, y lo más robusto es combinarla con Object.create(null), validación de esquema y el flag --disable-proto.
¿JSON.parse contamina el prototipo por sí solo?
No. JSON.parse('{"__proto__":{}}') crea __proto__ como propiedad propia del objeto resultante y no toca el prototipo global. La contaminación se produce en un paso posterior, cuando un merge, un clonado profundo o una asignación con notación de corchetes copian esa clave hacia Object.prototype. El riesgo está en el sink, no en el parseo.
¿Cómo llega prototype pollution a RCE?
Necesita un gadget: una porción de código que lea una propiedad heredada y la use para ejecutar. En Node.js los gadgets habituales están en child_process (opciones shell, env, NODE_OPTIONS con --require) y en motores de plantillas. La investigación Silent Spring (USENIX 2023) sistematizó estas cadenas y Kibana (CVE-2019-7609) es el caso público de referencia.
¿Qué librerías han sido afectadas históricamente?
Entre las más conocidas están lodash (CVE-2019-10744), jQuery (CVE-2019-11358), el parser ini (CVE-2020-7788), tough-cookie (CVE-2023-26136) y qs en Express (CVE-2022-24999). Dado que estas utilidades son dependencias transitivas muy extendidas, una única versión vulnerable en el árbol de dependencias puede exponer a toda la aplicación.
Recursos relacionados
- Qué es la inyección SQL: ataque y prevención
- Inyección de comandos (command injection)
- Qué es IDOR: broken access control
- Pentesting de APIs REST y GraphQL
- Las 5 vulnerabilidades web más comunes en 2025
Auditar prototype pollution con Secra
Secra realiza auditorías de aplicaciones web y APIs alineadas con OWASP, incluyendo la búsqueda específica de prototype pollution en cliente y servidor. Analizamos merges recursivos, parsers de entrada, clonados profundos y el árbol de dependencias npm para localizar tanto el sink como el gadget que lo convierte en RCE, DoS o bypass de autorización.
Entregamos hallazgos priorizados, pruebas de concepto reproducibles y recomendaciones de hardening concretas para vuestro stack Node.js: objetos sin prototipo, validación con Ajv, flags --disable-proto y actualización de dependencias vulnerables. Podéis conocer el alcance en nuestro servicio de auditoría web y móvil o escribirnos desde /es/contacto/ para planificar una revisión adaptada a vuestra plataforma.
Sobre el autor
Equipo de Secra Solutions
Ethical hackers certificados OSCP, OSEP, OSWE, CRTO, CRTL y CARTE, con más de 7 años de experiencia en ciberseguridad ofensiva. Autores de los CVE-2025-40652 y CVE-2023-3512.

