Confusión de tipos en JavaScript: cómo evadir la validación de entrada (y cómo corregirlo)
Alessio Della Libera
3 de noviembre de 2021
0 minutos de lecturaEn una publicación anterior del blog, mostramos cómo la manipulación de tipos (o confusión de tipos) puede usarse para escapar de los entornos aislados de plantillas y provocar vulnerabilidades de cross-site scripting (XSS) o de inyección de código.
Uno de los principales objetivos de esta investigación fue explorar (en el ecosistema de JavaScript) cómo y si es posible evadir algunas correcciones de seguridad o validaciones de entrada mediante un ataque de confusión de tipos (es decir, al proporcionar un tipo de entrada inesperado).
Comenzamos nuestra investigación analizando cómo se corrigen vulnerabilidades de seguridad comunes, como la contaminación de prototipos y XSS. Para ello, aprovechamos los datos de nuestra base de datos de vulnerabilidades Snyk Intel, la mayor base de datos de vulnerabilidades de código abierto de la industria. Gracias a esta información, pudimos identificar algunos patrones que distintos responsables de mantenimiento usan para prevenir ciertas clases de vulnerabilidades.
Ahora es momento de analizar las vulnerabilidades por confusión de tipos. En esta publicación del blog, buscamos mostrar escenarios comunes en los que es posible evadir la sanitización y validación de entradas al proporcionar un tipo de entrada inesperado. También compartiremos ejemplos de corrección y sugerencias para quienes mantienen proyectos de código abierto y buscan solucionar este tipo de evasiones de la validación de entradas.
Conceptos básicos de JavaScript
Antes de hablar de cómo un valor de tipo arreglo puede usarse para evadir algunas validaciones de entrada y provocar una posible vulnerabilidad de seguridad, repasemos algunos conceptos fundamentales sobre el funcionamiento de JavaScript. Serán muy útiles más adelante, especialmente cuando analicemos el caso de estudio sobre contaminación de prototipos.
Comparación de valores: === y ==
En JavaScript, hay dos operadores diferentes que se pueden usar para comparar valores:
Igualdad estricta
===Igualdad no estricta
==
Una de las principales diferencias entre ellos es que el operador === siempre devuelve false si los operandos son de tipos diferentes (no realiza ninguna forma de coerción). En el caso del operador ==, si los operandos son de tipos diferentes, primero se convierten a un tipo común y luego se comparan. Para obtener más detalles sobre el funcionamiento de estos operadores, consulta la documentación oficial del operador de igualdad estricta y del operador de igualdad no estricta.
El siguiente ejemplo muestra este comportamiento:
Cuando se comparan operandos con el mismo valor y tipo, los operadores == y === devuelven true (líneas 2 y 3). Sin embargo, si comparamos un arreglo cuya representación como cadena coincide con la del otro operando, solo devolverá true si se usa el operador ==. En la línea 6, vemos que el valor ["test"] primero se convierte a su representación como cadena y luego se compara con el otro operando "test". Cuando los operandos son de tipos diferentes, el operador === siempre devuelve false (línea 7).
Nota: Lo anterior también se aplica a los operadores de desigualdad !== y !=.
Algunas formas de obtener la representación como cadena de un valor son:
llamar al método
toString()(línea 2)convertir a una cadena mediante la función
String(línea 2)concatenar el valor con una cadena vacía mediante el operador
+(línea 3)
En el caso de un valor de tipo arreglo, su representación como cadena es la concatenación de sus elementos separados por comas.
Accesor de propiedades
Otra característica importante de JavaScript es que, al acceder a las propiedades de un objeto mediante la notación de corchetes, es posible especificar valores de distintos tipos (no solo cadenas). Si el valor no es una cadena (ni un Symbol), primero se convierte a una cadena y luego se usa para acceder a la propiedad del objeto.
Veamos el siguiente ejemplo:
Como podemos ver, la clave usada para acceder a una propiedad puede ser de distintos tipos. En la línea 4, la propiedad ["test"] primero se convierte a una cadena (es decir, al valor "test") y el resultado de esta operación se usa como clave para acceder a la propiedad del objeto. De hecho, si usamos la cadena "test" en la línea 6, podemos obtener el valor asignado en la línea 4.
Del mismo modo, si usamos un objeto vacío {} (línea 9), cuya representación como cadena es [object Object], para escribir un valor en un objeto, luego podemos acceder al mismo valor usando directamente la cadena [object Object] (línea 11).
Métodos de String y Array
Hay algunos métodos integrados definidos tanto en String como en Array que tienen el mismo nombre. Algunos ejemplos son includes, indexOf, lastIndexOf, etc.
¿Por qué es relevante esto para esta publicación del blog? Estos métodos se comportan de forma diferente según el tipo de entrada al que se aplican. Si se usa uno de estos métodos para validar la entrada del usuario, la validación podría ser vulnerable a una posible evasión si se proporciona una entrada de otro tipo (por ejemplo, un arreglo) y no se realizan comprobaciones.
El siguiente ejemplo muestra cómo se comportan de forma diferente los métodos integrados definidos tanto en String como en Array:
Nota: Todos los valores del ejemplo ("<script>", ["<script>"] y [["<script>"]]) tienen la misma representación como cadena.
Por ejemplo, en la línea 3, el método que se llama es String.prototype.indexOf(). Este método comprueba si el carácter < está en la cadena y luego devuelve el índice de su primera aparición; de lo contrario, devuelve -1.
Sin embargo, si la entrada es un arreglo, el método que se llama es Array.prototype.indexOf(), que comprueba si el elemento < está en el arreglo. Como el arreglo solo tiene un elemento (la cadena "<script>"), la función devuelve -1, lo que impide la posible sanitización de ese carácter.
Resumen de conceptos fundamentales de JavaScript
Resumamos lo que hemos visto:
Si los operandos son de tipos diferentes, el operador
===siempre devuelvefalseEs posible usar claves de distintos tipos para acceder a las propiedades de objetos (cuando se usa la notación de corchetes)
Algunos métodos integrados definidos tanto en los tipos
StringcomoArray(comoindexOfoincludes) pueden comportarse de forma diferente según el tipo de entrada
Antes de continuar, veamos los siguientes escenarios en los que se usan algunos de los métodos anteriores para validar la entrada del usuario.
La siguiente comprobación podría no ser suficiente para impedir que se establezca la clave isAdmin en el objeto obj (suponemos que el usuario controla la variable prop):
Si el valor de prop cambia a "isAdmin", el ejemplo anterior generará una excepción.
A continuación, se muestra otro ejemplo de una comprobación «débil» que busca prevenir un posible XSS detectando si la entrada contiene ciertos caracteres peligrosos:
Es importante tener en cuenta que estas evasiones solo son posibles en determinadas condiciones. Por ejemplo, la entrada debe provenir de fuentes específicas (consulta la siguiente sección) y no se deben llamar en ella métodos integrados definidos únicamente en String; de lo contrario, la aplicación devolverá un error o una excepción (porque no están definidos para otros tipos).
En el siguiente ejemplo, se llama al método toLowerCase() en la entrada. Aunque ya vimos que es posible evadir la comparación === con un valor de tipo arreglo, si se proporciona un arreglo, la aplicación generará una excepción porque el método toLowerCase() solo está definido para cadenas.
Cómo obtener valores de tipo arreglo
En este punto, quizá te preguntes cómo podemos obtener valores de tipo arreglo a partir de datos remotos.
Hay distintas formas de obtener valores de tipo arreglo:
Del lado del servidor: Si usas el popular framework express, los valores de
req.queryoreq.body(si se usa el middlewareexpress.json()) también pueden analizarse como arreglos u objetos (no solo como cadenas).Del lado del cliente: Datos provenientes de la API
postMessage.
Del lado del servidor
En las solicitudes GET, si usas el popular framework express y el valor proviene de req.query, es posible obtener valores de tipo arreglo mediante la notación parameter[]=value. En general, esto puede aplicarse a cualquier aplicación que use la popular biblioteca qs para analizar la querystring.
En las solicitudes POST, si se usa el middleware express.json(), el cuerpo de la solicitud se analiza como un objeto JSON.
La siguiente aplicación express muestra cómo es posible obtener valores de tipo arreglo:
Del lado del cliente
Los datos provenientes de la API postMessage también pueden ser de distintos tipos (no solo cadenas):
Para probar el código anterior, abre la consola de desarrollador del navegador y ejecuta: window.postMessage({name: ["array"]}, "*"). Deberías obtener el siguiente resultado:

Resultados
Como parte de esta investigación, algunos de los problemas que encontramos y divulgamos son:
Módulo | Vulnerabilidad | Aviso de Snyk | CVE |
|---|---|---|---|
object-path | Contaminación de prototipos | CVE-2021-23434 | |
immer | Contaminación de prototipos | CVE-2021-23436 | |
mpath | Contaminación de prototipos | CVE-2021-23438 | |
set-value | Contaminación de prototipos | CVE-2021-23440 | |
edge.js | Cross-site scripting (XSS) | CVE-2021-23443 | |
jointjs | Contaminación de prototipos | CVE-2021-23444 | |
datatables.net | Cross-site scripting (XSS) | CVE-2021-23445 | |
teddy | Cross-site scripting (XSS) | CVE-2021-23447 |
Nos comunicamos de forma responsable y privada con varios responsables de mantenimiento, siguiendo nuestro proceso de divulgación de vulnerabilidades (algunos hallazgos aún están en proceso de divulgación).
Sobre todo, queremos agradecer a todos los responsables de mantenimiento con quienes nos comunicamos por su tiempo.
En los siguientes casos de estudio, explicaremos cómo fue posible evadir una corrección para la contaminación de prototipos y una validación de entrada contra XSS al proporcionar valores de tipo arreglo.
Caso de estudio: contaminación de prototipos en object-path
object-path es una biblioteca que se usa para acceder a propiedades anidadas mediante una ruta. La biblioteca también acepta una ruta en forma de arreglo, lo que significa que es posible proporcionar rutas con el formato ['part1', 'part2', etc.]. Esta biblioteca ya era vulnerable a la contaminación de prototipos (CVE-2020-15256). La corrección introducida es la siguiente:
Esta corrección genera correctamente una excepción cuando la ruta contiene uno de esos componentes peligrosos (y son cadenas). De hecho, con la siguiente carga, la función generará un error:
Sin embargo, como vimos antes:
Se puede usar cualquier tipo de clave para acceder a las propiedades de objetos mediante la notación de corchetes
El operador
===devuelvefalsesi los operandos son de tipos diferentes. La condicióncurrentPath === '__proto__'devolveráfalsesicurrentPathes['__proto__'](lo mismo sucede conconstructor).
Esto significa que envolver la clave peligrosa en un arreglo de un solo elemento (la clave misma) permitirá evadir la corrección y seguirá provocando contaminación de prototipos:
Corrección
Snyk se comunicó en privado con el responsable de mantenimiento el 25 de agosto de 2021 y el problema se corrigió rápidamente el 27 de agosto de 2021 en v0.11.6. La corrección introducida evita este escenario al convertir los componentes de la ruta a cadenas (si no son de tipo número o cadena) antes de comprobarlos:
La carga útil anterior ya no funcionará:
Caso práctico: Cross-Site Scripting (XSS) en edge.js
edge.js es un motor de plantillas para Node.js. Esta biblioteca incluye funcionalidades que se pueden habilitar para prevenir vulnerabilidades de seguridad, como XSS. En particular, según la documentación, «el resultado de la interpolación (el código entre llaves) se escapa como HTML para evitar ataques XSS». La función original encargada de escapar los caracteres HTML peligrosos para prevenir XSS es la siguiente:
Como podemos ver, input solo se escapa como HTML si es de tipo string. Esto significa que, si la entrada controlada por el usuario es de tipo objeto (es decir, un arreglo) y no es un SafeValue, aunque se use {{ }} en la plantilla, se devuelve sin escapar (sin errores ni excepciones) y, por lo tanto, puede provocar una vulnerabilidad XSS.
Para demostrar cómo se puede explotar esto, veamos la siguiente aplicación web, que usa esta biblioteca como motor de plantillas para mostrar un valor controlado por el usuario en una plantilla:
El contenido del archivo views/welcome.edge es el siguiente:
Como vimos antes, los datos provenientes de ciertas fuentes también pueden ser de distintos tipos. Al visitar la URL http://localhost:3000/test?name=%3Cimg%20src=x%20onerror=%27alert(1)%27%20/%3E, la función escapa la entrada. Sin embargo, al visitar esta otra URL (observa los paréntesis []) http://localhost:3000/test?name[]=%3Cimg%20src=x%20onerror=%27alert(1)%27%20/%3E, se puede provocar un XSS porque el parámetro req.query.name se analizará como un arreglo y, por lo tanto, no se escapará.
Solución
Snyk se puso en contacto de forma privada con la persona encargada del mantenimiento el 1 de septiembre de 2021, y el problema se solucionó rápidamente en v5.3.2. La corrección implementada aborda este escenario al escapar todas las entradas que no sean de tipo SafeValue:
Otras formas posibles de prevenir estas situaciones son convertir la entrada en una cadena antes de escaparla (y así evitar comprobar el tipo de entrada, como en el caso anterior) o devolver una cadena vacía (o un mensaje de error) si la entrada no es de tipo string.
Conclusiones para...
Desarrolladores
Al usar el operador === para realizar una sanitización, es importante asegurarse de que ambos operandos sean del mismo tipo. Al escapar entradas, también es importante manejar el caso en que la entrada no sea de tipo string (sobre todo si no está documentado cómo se comporta la función con valores que no son cadenas).
Responsables de mantenimiento
Si una función o API se encarga de sanitizar entradas y no contempla el tipo de entrada, conviene documentar este comportamiento para evitar confusiones. Es posible que la persona encargada del mantenimiento solucione algunos problemas de sanitización (como escapar la entrada si es una cadena), pero no otros (como manejar el caso en que la entrada no sea una cadena).
Investigadores de seguridad
Nos centramos en las vulnerabilidades de contaminación de prototipos y XSS, pero creemos que también puede haber otros casos o escenarios en los que se use este vector de ataque para eludir las sanitizaciones existentes y provocar posibles problemas de seguridad. Si encuentras problemas similares (o cualquier otra vulnerabilidad de seguridad) en un proyecto de código abierto que cuente con el apoyo de nuestro programa, no dudes en enviárnoslos mediante el formulario de divulgación de vulnerabilidades de Snyk.
Referencias
Contaminación de prototipos: Arteau, Oliver. «JavaScript prototype pollution attack in NodeJS application». GitHub, 26 de mayo de 2018
No conoces JavaScript: Tipos y gramática - Capítulo 4: Coerción
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.



