Categorisation of ECMAScript(+event loops) Threats

# David Bruant (13 years ago)

This post is not intentioned to be a perfect and final guide, but rather a conversation starter. It's mostly triggered by what Andrea described as a potential attack (assigning Object.prototype.get).

Description of the JS runtime

To a first approximation (ignoring things like alert/prompt and weird window resize event behaviors), a JavaScript runtime contains a heap, a call stack and a message queue. Messages accumulate in the queue and are treated one after the other. The time during which one message is treated is called a turn. Messages are queued when a user clicks and a listener listens or when a timeout fires, etc. A message contains a function which, upon being called may call other functions (hence the call stack). Functions manipulate memory, including things in the heap. The heap contains a bunch of objects being accessible either from the global object or the scope of the function being called.

Some properties

Environment Mutability

Until ES5, pretty much everything in the heap could be modified at any turn by anyone; new properties could be added/removed from the global object, or the Object.prototype object for instance. Sharing access to an object pretty much meant giving away the possibility to anyone with access to this object to mess up with it. ES5 introduces a couple of constructs like Object.preventExtensions and Object.defineProperty(,,{configurable: false, writable: true|false}) to enable a programmer to share some object but have some properties enforced. It is probably the first time there are constructs that act on objects to create permanent effects on them.

During a turn

Stating the obvious, but the function running in a turn has the power to do anything to whatever it has access to. Among the possible changes, it's possible to Object.freeze any object the function has access to. When some JS code is running during a turn, it can't be preempted by some other code. The code has to end.

First turn

There is a first turn.

(Partial) conclusion

If malicious code runs, it can do whatever is possible during its turns. The role of a defenser will be to reduce "whatever is possible" to "nothing harmful". First reaction, if the malicious code runs first, too bad, it can edit anything in the JavaScript environment it has access to, including modifying Object.prototype. The careful attacker will freeze Object.prototype after having modified it. The opposite is true too. If the defender runs first, it can defend its environment. If the defender doesn't want Object.prototype to be modified, it can freeze it. If it wants to add polyfills then freeze everything, that's also a decent idea. If during the first turn no one modifies anything in an harmful or defensive way, the decision to attack or defend is let to the code running in the next turn and so on.

Categorisation of threats

Threats that are unprotectable against

Programming languages or APIs sometimes leave holes by design that even the most skilled programmers couldn't write defensive programs against. If I recall, ES5 strict mode has gotten rid of all of them with the notable exception of mutable Date.prototype that provides a covert channel. SES is an example of script that does everything it can to protect the JS environment (including fixing or working around almost-comformant environments bugs!)

Threats that can be protected against

From everything I have said, one clear thing is that protecting the JavaScript is an opt-in. If you do not opt-in to a secure environment, you're left to be a prey by default. The list of potential attacks is probably endless. Adding an Object.prototype.get function and waiting for people to use Object.defineProperty is such an attack. Putting an accessor on Array.prototype[0] is also a possibility.

What does it mean for the language evolution?

The first category needs to be taken care of and is. Next features are carefully thought out to avoid unprotectable threats. The second category is a bit more debatable. There is tension due to the fact that the vast majority of JS developers aren't aware that JavaScript security requires an opt-in like SES and as a consequence are victim of whatever buggy or malicious code they happen to add to their webpages.

Given that security requires an opt-in anyway, is it worthwhile to modify the language (like Object.defineProperty) to protect for some cases but not others? I would tend to say no.

Regarding Andrea's specific case, I'd like to mention that proxy handlers are also based on [[Get]]. The inheritance is actually used in some cases for trap reuse (it was even more the case in the previous design, but still is the case). Should inheritance be prevented here too and ruin a valid use case? Just because an attacker might add an Object.prototype.someTrap and the defender might have forgotten to protect against it? I don't think it's worth the cost in this case.