
TC39 proposal for mitigating prototype pollution
Autores: Santiago Díaz (Google)
Champion: Shu-yu Guo (Google)
Etapa: 1
freeze, seal y preventExtensions
Esta propuesta busca mitigar una vulnerabilidad a nivel de lenguaje conocida como contaminación de prototipos con un mecanismo que complementa las primitivas de congelación y un mecanismo para hacer que la mayoría de las bases de código sean compatibles con él. Describe una característica de adopción voluntaria que hace que los prototipos estén disponibles únicamente a través de APIs de reflexión. Al hacerlo, la sentencia obj[key] ya no puede acceder a los prototipos. Las bases de código compatibles con esta característica son más intencionales en la forma en que usan los prototipos.
Las vulnerabilidades de PP permiten a los atacantes manipular objetos que no controlan o a los que no tienen acceso en tiempo de ejecución. Esta primitiva de 'acción fantasmagórica a distancia' puede utilizarse para cambiar la forma de otros objetos y sobrescribir sus propiedades, contaminando así objetos en el tiempo de ejecución.
Los objetos contaminados invalidan las suposiciones subyacentes del código que de otro modo sería seguro/correcto y pueden conducir a la ejecución arbitraria de código y a una amplia gama de otros problemas de seguridad en bases de código JS. Los errores de contaminación de prototipos se manifiestan a menudo en aplicaciones web, pero también afectan a los tiempos de ejecución JS no web.
Las propiedades de los objetos en JS pueden ser escritas por cualquier código que pueda hacer referencia a ellas. En particular, si muchos objetos dependen de una propiedad compartida, cualquiera de ellos puede realizar cambios en todos los demás.
Una propiedad especial de PP es que se trata de un ataque solo de datos, que permite lograr la ejecución de código puramente a través de datos. Por ejemplo, véase el siguiente código vulnerable y un exploit correspondiente:
// source is attacker-controlled
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
if(target[key] === undefined) {
target[key] = {};
}
target[key] = merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// User input comes as a string
const userSuppliedObj = JSON.parse('{"__proto__": {"polluted": true}}');
// Trigger prototype pollution
merge({}, userSuppliedObj);
// Create a brand new object
const newObj = {};
// Has polluted property
console.log(newObj.polluted); // true
Tenga en cuenta que el exploit es capaz de contaminar la creación de nuevos objetos sin inyectar ningún código externo.
Debido a esta propiedad especial, las mitigaciones modernas contra problemas de ejecución de código —como la Content Security Policy o Trusted Types— se quedan cortas a la hora de proteger contra PP, ya que se centran en hacer cumplir la procedencia del código.
Nótese que los ataques solo de datos son relevantes en situaciones donde el código que se ejecuta en la VM es de confianza y la ejecución arbitraria de código tiene impacto en la seguridad.
freeze, seal y preventExtensionsLas primitivas de congelación existentes sufren problemas de diseño significativos que hacen poco probable su adopción generalizada. Pueden ser útiles para usuarios expertos, pero no son adecuadas para ser implementadas por la mayoría de los desarrolladores, quienes razonablemente esperan que los prototipos sean mutables:
Las APIs de congelación sufren el error de sobrescritura y otras inconsistencias que introducen errores en bases de código existentes, haciendo que lancen excepciones o, peor aún, fallen silenciosamente en modo no estricto (sloppy mode). Una investigación anterior del error de sobrescritura concluyó que este se activa en ~10% de las bases de código en modo estricto y en 20% en modo no estricto. La investigación se abandonó poco después.
Las APIs de congelación otorgan a los desarrolladores la pesada responsabilidad de saber qué prototipos deben congelarse para mantener una base de código segura, asumiendo que los desarrolladores son expertos en seguridad. Estas APIs describen el qué pero no el cómo de la seguridad. Congelar Object ciertamente no es suficiente, ya que muchos exploits abusan de Array. ¿Qué pasa con Error, Date, Reflect o Proxy? ¿O con los futuros tipos integrados? Las APIs de congelación no ofrecen respuestas a estas preguntas.
Las APIs de congelación asumen un punto de congelación estable: un momento fijo en tiempo de ejecución en el que los prototipos se han asentado y pueden congelarse. En la práctica, este punto es volátil y cambia con el tiempo en bases de código que se desarrollan activamente. Aunque hoy se puede encontrar ese punto en muchas aplicaciones, la incorporación de nuevas dependencias, polyfills, cambios en la estructura del código y funciones avanzadas como el hotswap y las herramientas de desarrollo convierten los puntos de congelación en un objetivo móvil.
Las APIs de congelación no pueden proteger la cadena de prototipos completa. En JS, los objetos pueden añadirse o eliminarse de la cadena de prototipos en cualquier momento. Para proteger toda la cadena, siempre hay que recordar congelar los objetos que se añaden a la cadena, un proceso propenso a errores. Cuando se eliminan de la cadena, ya no se pueden descongelar.
En pocas palabras: una característica que expone los prototipos únicamente a las APIs de reflexión. Si los prototipos no estuvieran disponibles a través de propiedades como __proto__ o prototype, no estarían expuestos a los problemas solo de datos.
Esto se entiende mejor con un ejemplo: la sentencia obj[one][two] = value es vulnerable a PP a través de obj.__proto__.polluted. Si se elimina la propiedad Object.prototype.__proto__, la misma sentencia ya no es vulnerable porque no puede hacer uso de la única otra forma de llegar a los prototipos, que es obj.constructor.prototype.polluted. Nótese que prototype no puede eliminarse.
Esta propuesta puede implementarse proporcionando APIs de reflexión y creando una nueva característica de encapsulación de adopción voluntaria que elimina las propiedades de prototipo. A continuación se describe cada paso.
__proto__ es un nombre de propiedad heredado que puede eliminarse, pero el slot interno que hay detrás todavía puede leerse mediante Object/Reflect.getPrototypeOf y escribirse mediante Object/Reflect.setPrototypeOf, que simplemente seguirán haciendo que esta propiedad sea accesible para el código que ya está en ejecución.
Proponemos la creación de nuevas APIs para prototype, por ejemplo getClassPrototypeOf y setClassPrototypeOf, que permitirían eliminar este nombre de propiedad sin cambiar en nada la forma en que esta propiedad especial funciona y es compatible con la VM.
Las APIs de reflexión pueden tener polyfills, lo que permite que las bases de código endurecidas funcionen en todos los navegadores, incluidas las versiones antiguas.
Una nueva 'característica de encapsulación' de adopción voluntaria en la que no se crean nombres de propiedad para las funciones getter y setter de los slots de prototipo, lo cual ahora es posible porque las referencias a esas propiedades pueden utilizar en su lugar las APIs de reflexión.
La característica se habilita mediante un flag fuera de banda:
X-Encapsulate-Prototype: true--encapsulate-prototypeCuando la encapsulación está deshabilitada, los prototipos están disponibles tanto a través de propiedades como de APIs de reflexión.
Cuando la encapsulación está habilitada, los prototipos solo están disponibles a través de APIs de reflexión, habiendo eliminado tanto __proto__ como prototype.
La encapsulación también incluye la siguiente refactorización automática:
Cuando la encapsulación está habilitada, los motores JS que cargan nuevo código fuente activan un paso adicional en sus fases de parseo que registra toda notación de punto a propiedades de prototipo como si fueran llamadas a sus APIs de reflexión. Este paso puede implementarse de forma eficiente y permite que las bases de código con dependencias de terceros, transitivas o cargadas dinámicamente sean compatibles con la encapsulación.
En el futuro, este cambio allanará el camino para marcar prototype como obsoleto.
Las propiedades de prototipo podrían simplemente ser undefined cuando la encapsulación está habilitada, pero podrían lanzar un error cuando se intente leerlas o escribirlas. Esto significaría fallar más rápido y de forma ruidosa y permitiría probar las migraciones a las APIs de reflexión/encapsulación.
Esto implica hacer que los getters y setters de __proto__ y prototype sean condicionales a la encapsulación, mediante un host hook, de la misma manera que la función eval lanza una excepción bajo la Content Security Policy.
El código que depende del acceso computado a propiedades para hacer referencia a prototipos no es compatible con la encapsulación ni con la refactorización automática. Debe refactorizarse para referirse explícitamente a los prototipos cuando se utilicen. Esta refactorización en realidad hace que el código exprese su intención, lo que hace visibles los patrones peligrosos para el análisis estático. En la práctica, las bases de código con esta característica suelen ser frameworks de reflexión, herramientas de depuración y otros casos de uso intensivos en reflexión que probablemente ya son conscientes de cómo usan los prototipos.
Las bases de código que utilizan la palabra prototype para definir propiedades personalizadas no son compatibles. Estas bases de código pueden hacerse compatibles con la encapsulación si esta propiedad se establece/obtiene siempre mediante notación de corchetes. Históricamente y según las consultas al HTTP Archive, hay un pequeño porcentaje de bases de código que son incompatibles por esta razón.
Algunos cambios en la propiedad constructor también pueden tener acción fantasmagórica a distancia. Durante nuestra investigación, no hemos encontrado vulnerabilidades prácticas afectadas por esto.
El listón es bastante alto para que este ataque funcione: al igual que en PP, se debe encontrar una aplicación con gadgets para escribir y leer propiedades arbitrarias. Pero en la contaminación de constructores, el gadget de lectura debe leer de constructor.polluted en lugar de polluted. Esto reduce drásticamente el número de gadgets útiles.
Parte del JS minimizado puede ser incompatible con el modo de encapsulación, porque el acceso estático a propiedades podría ser minificado a acceso computado. Hemos consultado el HTTP Archive para obtener una estimación de esto en la práctica. La siguiente tabla muestra que las páginas con este comportamiento se mantienen constantemente por debajo del 1% durante los últimos 12 meses en todas las páginas rastreadas con un navegador de escritorio:
Google ha observado una tendencia al alza en los errores enviados a nuestro Programa de Recompensas por Vulnerabilidades: 1 en 2020, 3 en 2021 y 5 hasta ahora en 2022. Hemos identificado varios más en nuestra investigación interna.
Los ejemplos de vulnerabilidades incluyen:
Esperamos que el número de aplicaciones vulnerables crezca a medida que las aplicaciones JavaScript se desplieguen en más entornos (por ejemplo, Electron, Cloudflare Workers, etc.). Por lo tanto, se requiere una solución a nivel de lenguaje para mitigar los ataques en todos los entornos.
| Tabla | Documentos que acceden dinámicamente a __proto__ o constructor | Número total de documentos rastreados | Proporción |
|---|
| 2023_03_01_desktop | 5,407,936 | 609,469,458 | 0.89% |
| 2023_02_01_desktop | 4,842,383 | 549,089,708 | 0.88% |
| 2023_01_01_desktop | 5,283,826 | 589,519,160 | 0.90% |
| 2022_12_01_desktop | 5,161,471 | 577,073,883 | 0.89% |
| 2022_11_01_desktop | 5,023,169 | 561,726,239 | 0.89% |
| 2022_10_01_desktop | 4,393,377 | 476,880,624 | 0.92% |
| 2022_09_01_desktop | 4,239,257 | 466,278,762 | 0.91% |
| 2022_08_01_desktop | 4,259,814 | 463,784,047 | 0.92% |
| 2022_07_01_desktop | 3,011,137 | 339,468,615 | 0.89% |
| 2022_06_01_desktop | 2,301,317 | 257,501,222 | 0.89% |
| 2022_04_01_desktop | 2,368,577 | 263,144,657 | 0.90% |
| 2022_03_01_desktop | 2,319,518 | 259,249,013 | 0.89% |