Giter Club home page Giter Club logo

proposal-symbol-proto's Introduction

Prototype Pollution Mitigation / Symbol.proto

Authors: Santiago Díaz (Google)

Champion: Shu-yu Guo (Google)

Stage: 1

TOC

tl;dr

This proposal seeks to mitigate a language-level vulnerability known as prototype pollution with a mechanism that complements freeze primitives and a mechanism to make most code bases compatible with it. It describes an opt-in feature that makes prototypes available only through reflection APIs. By doing so, the statement obj[key] can't access prototypes anymore. Code bases compatible with this feature are more intentional about the way they use prototypes.

Problem Description

Spooky action at a distance

PP vulnerabilities allow attackers to manipulate objects they don't control or don't have access to at runtime. This 'spooky action at a distance' primitive can be used to change the shape of other objects and override their properties, thereby tainting objects in the runtime.

Tainted objects invalidate the underlying assumptions of code that would otherwise be safe/correct and can lead to arbitrary code execution and a wide range of other security issues in JS code bases. Prototype pollution bugs express themselves often in web applications, but also affect non-web JS runtime.

Object properties in JS are writeable by any code that can reference them. In particular, if many objects rely on a shared property, any one of them can enact changes on all others.

Data-only attacks

A special property of PP is that it is a data-only attack, allowing code execution to be attained purely through data. For instance, see the following vulnerable code and a corresponding exploit:

// 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

Note that the exploit is able to taint the creation of new objects without injecting any foreign code.

Because of this special property, modern mitigations against code execution issues -like the Content Security Policy or Trusted Types- fall short of protecting against PP, as they focus on enforcing code provenance.

Note that data-only attacks are relevant to situations where the code running on the VM is trusted and arbitrary code execution has security impact.

Issues with freeze, seal and preventExtensions

Existing freezing primitives suffer from significant design issues that make them unlikely to be widely adopted. They can be useful to expert users, but are not suitable to be deployed by the majority of developers, who reasonably expect prototypes to be mutable:

The override mistake

Freeze APIs suffer from the override mistake and other inconsistencies which introduce bugs in existing code bases, making them throw or worse, silently fail in sloppy mode. A previous investigation of the override mistake concluded that the override mistake triggers on ~10% of code bases in strict mode and 20% in sloppy mode. The investigation was dropped shortly after.

Coarse granularity

Freeze APIs give developers the heavy responsibility of knowing which prototypes should be frozen to maintain a secure code base, assuming that developers are security experts. These APIs describe the what but not the how of security. Freezing Object is certainly not good enough, as many exploits abuse Array. What about Error, Date, Reflect or Proxy? Or future built-in types? Freeze APIs provide no answers to these questions.

Freezing points

Freeze APIs assume a stable freezing point: a fixed moment at runtime where prototypes have settled and can be frozen. In practice, this point is volatile and changes over time in code bases that are actively developed. While one can find such a point in many applications today, the addition of new dependencies, polyfills, code structure changes and power features like hotswapping and developer tools make freezing points a moving target.

Application types

Freeze APIs can't protect the full prototype chain. In JS, objects can be added or removed from the prototype chain at any point in time. To protect the full chain, one should always remember to freeze objects that are added to the chain, an error-prone process. When they are removed from the chain, they cannot be made unfrozen anymore.

Proposed solution

In a nutshell: a feature that exposes prototypes only to reflection APIs. If prototypes weren't made available through properties like __proto__ or prototype, they would not be exposed to data-only issues.

This is better understood through an example: the statement obj[one][two] = value is vulnerable to PP through obj.__proto__.polluted. If one deletes the Object.prototype.__proto__ property, the same statement is no longer vulnerable because it can't fit the only other way to reach prototypes, which is obj.constructor.prototype.polluted. Note that prototype can't be deleted.

This proposal can be implemented by providing reflection APIs and creating a new opt-in encapsulation feature that deletes prototype properties. A description of each step follows.

Provide reflection APIs

__proto__ is a legacy property name that can be deleted, but the internal slot behind it can still be read through Object/Reflect.getPrototypeOf and written through Object/Reflect.setPrototypeOf, which will simply continue to make this property accessible to code that is already running.

We propose the creation of new APIs for prototype, for example getClassPrototypeOf and setClassPrototypeOf, which would allow this property name to be deleted without changing in any way how this special property works and supports the VM.

Reflection APIs can be polyfilled, which allows hardened code bases to work in all browsers, including older versions.

Opt-in feature

A new opt-in 'encapsulation feature' where no property names are created for the getter and setter function of prototype slots, which is now possible because references to those properties can use reflection APIs instead.

The feature is enabled through an out-of-band flag:

  • In browser contexts, through an HTTP header like X-Encapsulate-Prototype: true
  • In other contexts, through a feature flag like --encapsulate-prototype

When encapsulation is disabled, prototypes are available via both properties and reflection APIs.

When encapsulation is enabled, prototypes are only available via reflection APIs, having deleted both __proto__ and prototype.

Encapsulation also includes the following automatic refactoring feature:

Automatic refactoring

When encapsulation is enabled, JS engines loading new source code enable an extra step in their parse phases that registers all dot-notation to prototype properties as if they were calls to their reflection APIs. This step can be implemented efficiently and allows code bases with third-party, transitive or dynamically-loaded dependencies to be compatible with encapsulation.

In the future, this change will pave the way for marking prototype as deprecated.

What does delete mean?

Prototype properties could simply be undefined when encapsulation is enabled, but they could throw an error when there are attempts to read/write them. This would mean failing faster and loud and would allow migrations to the reflection APIs/encapsulation to be tested.

This implies making the getters and setters of __proto__ and prototype conditional on encapsulation, by using a host hook, in the same way the eval function throws under Content Security Policy.

Incompatible code bases

Code that relies on computed property access to reference prototypes is not compatible with encapsulation or with automatic refactoring. It must be refactored to explicitly refer to prototypes when they are used. This refactoring actually makes the code express intent, which makes dangerous patterns visible to static analysis. In practice, code bases with this characteristic are usually reflection frameworks, debugging tools and other reflection-heavy use cases that are most likely aware of how they use prototypes.

Code bases that use the word prototype to define custom properties are not compatible. Such code bases can be made compatible with encapsulation if this property is always set/get through bracket notation. Historically and based on HTTP Archive queries, there is a small percentage of code bases that are incompatible for this reason.

Appendix

What about constructor pollution?

Some changes to the constructor property can also have spooky action at a distance. During our research, we have found no practical vulnerabilities affected by this.

The bar is significantly high for this attack to work: Like in PP, one must find an application with gadgets to both write and read arbitrary properties. But in constructor pollution the reading gadget must read from constructor.polluted instead of polluted. This dramatically reduces the number of useful gadgets.

Computed access in minimized JS

Some minimized JS may be incompatible with encapsulation mode, because static property access could be minified into computed access. We have queried the HTTP Archive to get an estimate of this in practice. The following table shows that pages with this behavior are consistently below 1% throughout the last 12 months for all pages crawled with a desktop browser:

Table Documents accessing __proto__ or constructor dynamically Total number of crawled documents Ratio
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%

Example vulnerabilities

Google has seen an upward trend in bugs submitted to our Vulnerability Rewards Program: 1 in 2020, 3 in 2021 and 5 so far in 2022. We have identified several more in our internal research.

Example vulnerabilities include:

  1. On the Web: Several XSS issues in services that should have been protected because they use Strict CSP. And a wide range of known vulnerable libraries.
  2. On the desktop: An bug in a Google-owned desktop application where users could be given a malicious JSON object that could allow local files to be leaked due to a pollution vulnerability. (Currently non-public, disclosure TBD.)
  3. In security features: Multiple bypasses in sanitizers, including Chrome's Sanitizer API, DOMPurify and the Closure sanitizer.
  4. In the browser: A Firefox sandbox escape leading to remote code execution.
  5. In NodeJS: Several RCEs have been discovered.

We expect the number of vulnerable applications will grow as JavaScript applications are deployed to more environments (e.g. Electron, Cloudflare Workers, etc). Therefore, a language-level solution is required to mitigate attacks in all environments.

proposal-symbol-proto's People

Contributors

salcho avatar sosukesuzuki avatar syg avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

proposal-symbol-proto's Issues

Avoid name "proto"

I understand that Symbol.prototype already exists but at same time __proto__ also exists and has a drastically different meaning for what "proto" is being used for. Something like Symbol.instanceProto might be clearer.

suggestion: make `Object.prototype` exotically reject new properties

You can't freeze Object.prototype without triggering the override mistake. But you could, in principle, make Object.prototype an exotic object whose [[Set]] fails (or, I guess, throws).

(Maybe also make Array.prototype an exotic whose [[Set]] fails for valid array indices.)

It's possible that would be viable without a new mode, so it's a lot more palatable to me than the ideas here. Would that solve enough of the problem, do you figure?

Say more about "deleting the `constructor` property"?

The current shape of this proposal says it would (when enabled) "delet[e] the __proto__ and constructor properties".

I know what "deleting __proto__" means: it's a single getter/setter pair on Object.prototype, and it can indeed be outright deleted before any code executes (as for example Deno already does). That makes sense.

I don't know what "deleting constructor" means. constructor is an own property of the prototype created along with any function, as in

Object.hasOwn((function(){}).prototype, 'constructor') // true

It's also a property of the prototypes of most built-ins, of course, including Object.prototype, Array.prototype, etc.

Is the proposal that this property would no longer be created, but instead a symbol-named property would be created in its place, and it would be moved on all built-ins? And also all static access of .constructor would be changed at parse time - even when accessing properties not created by the engine, like if I did x = { constructor: "something" }; x.constructor === 'something'? Or is there some other proposal?

Also, I've encountered security vulnerabilities involving dynamic access of __proto__ often enough to appreciate the scale of the security issues there, but I'm less familiar with examples involving dynamic access of constructor. Some concrete examples, and a sense of the relative scale of the two issues, would be very helpful. Especially since my intuition is that changing constructor is going to be a lot more problematic than changing __proto__.

Secure mode hazard

Enable secure mode when Symbol.proto/constructor is used at least once.

This breaks every bundled asset in the wild as soon as any relevant to irrelevant library uses Symbol.proto that, once bundled together with any other code that can't prevent that, or be aware of.

This is not an option at all if we consider the current state of the Web and its deployment.

Introduce a use secure-mode directive or an Object.enableSecureMode() API

AFAIK / IIRC directives are not an option anymore (for better or worse results) but directives can be easily scoped and are static, while Object.enableSecureMode() could be invoked by accident via Object[shenanigan]().

It was a bit awkward to find use "secure-mode" as option, but definitively it's not clear why or how Object.enableSecureMode() should work and where that mode is secured (in scope? globally? in latter case, see previous point).

Introduce an out-of-band mechanism to enable secure mode

This seems to be the only non-breaking option and it gives all the footguns to the site/application owner so I suggest going forward with this idea (if others are the only alternatives).

Ideally, this should / could be a CSP policy so that it can incrementally be added but that won't work on NodeJS or non HTTP based projects and I am afraid .sjs is already taken as extension so that the use "secure-mode" looks like the best option yet it would be a step backward into the language (imho the best solution to these exact problems though).

Alternative "Delete the __proto__ and * keys" non-viable

Global disabling of .prototype and .constructor keys is unlikely to be viable. Node does have --disable-proto that it wished to turn on by default and even then the ecosystem has struggled to make it work (last run of our ecosystems tests still had it failing). I think deleting of __proto__ is possible but harder than framed given attempt to do so already.

Unfortunately due to some specifications like web components properties on these are intended to be accessed and would require major shifts to make them change not just in specifications but also in polyfills etc. See https://html.spec.whatwg.org/multipage/custom-elements.html#element-definition

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.