Should You Trust JavaScript Execution?

JavaScript is one of the most dynamic scripting programming languages. This article underlines the security concerns associated with JS.

Andrea Giammarchi
JavaScript in Plain English

--

Among scripting programming languages, JS is surely one of the most dynamic ones, or better, one of those languages where one could define, intercept, override, pretty much everything, including the crypto.subtle namespace used to encrypt and decrypt through secrets/passwords anything.

This fact is usually underestimated unless we work on a project that would like to ensure that users cannot leak data, be fingerprinted, make them immune to spyware while surfing, and so on.

Sure thing though, “one gotta trust the scripts that land on a website”, or on your back-end project, but the reality is that the Ads industry, that mostly nobody directly controls, starting from JS Frameworks’ developers, would do everything it can to make our surfing profitable, and it needs tracking, cookies, or similar workarounds, fingerprinting, and, from time to time, use extremely evil code (I work in this field, btw, fighting against evil code, and we also try to protect users from these kinds of exploits daily).

Add regular hack attempts through npm or similar, and here we are!

How bad is it?

The fact everyone believes that polluting native/builtin prototypes should be forbidden doesn’t mean that one cannot do that. Moreover, we can have all the linting rules in this world, but that’s just about our project, not about where our software will execute … can we see the difference?

const {[Symbol.iterator]: iterator} = Array.prototype;Object.defineProperty(
Array.prototype,
Symbol.iterator,
{
value() {
console.log('MitM', this);
return iterator.call(this);
// ^^^^^ keep reading
}
}
);

This tiny piece of code will allow anyone setting it up to:

  • read every ...spread operation any invoked function, method, or new Class(...args) might do
  • read every Array.from operation
  • read every for/of loop before it starts, or while it’s happening
  • read every Set or Map created through an array of entries
  • intercept every const [value, update] = useState(init) usage, as destructuring Arrays is also obviously affected
  • … and so on… giving access to pretty much every kind of data shared while the web application is used

Now, let’s add a typeof check to filter strings, define some common (historically ugly, pointless, broken, silly, …) /^[a-z0-9#$_+!?.-]{8,}$/i reg exp, together with a classic /^[^@]+@\S+$/, et voila’: no login is safe if authentication data is passed spreading. 🤯

These kinds of evil overrides can also be written behind code that makes it nearly impossible to detect, by polluting Function.prototype.toString and any other method we would use to detect if [native code] is there or not, so that basically, first evil comes, first evil serves.

⚠️ Not only Array

The Array.prototype trick is already interesting, but one could also poison every single prototype with the same ease (please don’t do this 🙏):

for (const key of Reflect.ownKeys(self)) {
// ignore non-classes maybe, typeof next!
if (!/^[A-Z]/.test(key))
continue;
const Builtin = self[key];
if (
typeof Builtin === 'function' &&
// ignore Proxy or others
'prototype' in Builtin &&
// filter by iterator presence
Builtin.prototype.hasOwnProperty(Symbol.iterator)
) {
// trap it
const {[Symbol.iterator]: iterator} = Builtin.prototype;
// override
Object.defineProperty(
Builtin.prototype,
Symbol.iterator,
{
value() {
// win 🥳
console.log('MitM', this);
return iterator.call(this);
// ^^^^^ keep reading
}
}
);
}
}
// tadaaaaaa 🎉
[...new Set([1, 2, 3])]
// two MitM logs for you

So yes, pretty much every iterable out there can be poisoned at its root, except for a few very good fellas. 🤠

function noProblem() {
console.log(...arguments);
}
// 1, 2, 3
noProblem(1, 2, 3);
const alsoNoProblem = (...args) => {
console.log.apply(console, args);
// ^^^^^^ keep reading
};
// 4, 5, 6
alsoNoProblem(4, 5, 6);
  • arguments object is not an Array, and it has had its own iterator for quite some time so it’s very secure to spread it around 🦄
  • ...rest parameters are just syntax hints for the engine, but these are not iterated when received, these are just an Array. However, if we spread them, they pass through the Array.prototype so that nothing changes if we spread these back; it’s still insecure, hence we need apply (and yet, keep reading)

⚠️ Not only Iterables

If we’re still following what the heck is going on, it might become obvious that poisoning any root prototype provides extremely powerful attacks I believe 99% of websites wouldn’t expect. Let’s see another example:

class User {
constructor(name) {
this.name = name;
}
auth(password) {
return fetch(
'/authenticate',
{
headers: {
Authorization: `Basic ${
btoa(`${this.name}:${password}`
)}`
}
}
);
}
}

Let’s quickly list all the things that could go wrong in these few lines of code, shall we?

  • Object.defineProperties(Object.prototype, {name, {get(){}, set(){}}, password: {get(){}, set(){}}, email: {get(){}, set(){}}}) could be used at any time to intercept all stupid classes out there (including mines) that just attach properties in the constructor, without needing to reach the User prototype at all
  • the fetch global function can be poisoned to intercept any kind of header, including those with OAuth credentials in it, or worse
  • the btoa global function also can be poisoned, and intercept credentials

In short, I am sorry to bring this down this way, but basically, we are doomed. 💣

⚠️ Not only JavaScript

This paragraph shouldn’t be needed if we understand that every programming language that targets JS would add an extra layer of indirection to code we don’t control so that the issue is even bigger and not easily solvable, but because I know that’s not usually the case, let’s show some practical example. 👍

TypeScript

There are various things that changes in production, once transpiled, and there’s zero extra trust TS will bring to the table, actually quite the opposite:

//TS
class User {
public name = '';
private pass = '';
}
// becomes this JS: busted
class User {
constructor() {
this.name = '';
this.pass = '';
}
}

Both public and private fields becomes constructor potential setters, so that any evil code intercepting those properties can poison at runtime the instance to retrieve name and password at any time.

Fair enough”, I hear someone yelling, “I’ll use real privates then!”, sure!

// TS
class User {
name = '';
#pass = '';
constructor(name: string) {
this.name = name;
}
}
// becomes this JS: busted
var _User_pass;
class User {
constructor(name) {
this.name = '';
_User_pass.set(this, '');
this.name = name;
}
}
_User_pass = new WeakMap();

This is the default that today TS offers me in its latest playground, targeting ES2017, which is a more than sensible target. (read: please don’t focus on “but ESNext target is pure” nonsense, thank you!)

That means that any evil code that pollutes at any point in time the WeakMap.prototype.set method, will be able to intercept exactly all those private variables or instances that we don’t want to expose … how “great” is that? 😢

I could go on for a while though:

// TS
function merge(a:object, b:object) {
return {...a, ...b};
}
// becomes this JS: busted
function merge(a, b) {
return Object.assign(Object.assign({}, a), b);
}

Here if any evil code would poison Object.assign, all TS instances will be exposed with ease and so it goes with everything else that becomes a Global.utility(...args) invoke.

Dart

I honestly don’t know Dart’s ecosystem, or if wild JS scripts could land unnoticed into a 100% Dart project, but sure enough its runtime is full of potential attacks.

For example, if we beautify the most basic example output I could find:

void main() {
for (int i = 0; i < 5; i++) {
print('hello ${i + 1}');
}
}

we’ll notice that the whole JS Dart’s runtime accesses repeatedly every global Object utility, including defineProperty and others.

Interestingly enough though, Dart’s runtime doesn’t seem to suffer the Symbol.iterator gotcha we previously discussed, mostly because it looks like its code is aiming to target also older browsers, maybe, but it also does operations such as Array.prototype.push.apply(target, arguments), falling back into the “everything easily exposed” category, just like TypeScript.

Wait … why is call or apply not safe, again?” Thanks for asking. 😎

const {call} = Function.prototype;
Object.defineProperty(
Function.prototype,
'call',
{
value(context, ...args) {
console.log(context, args);
return call.apply(this, [context, ...args]);
}
}
);

All I am trying to stress, is that everything that passes through JS primitives can, and might be, poisoned in a way or another: is this clear?

Node.js & Deno

Yes folks, yes! Everything I am talking about has its roots in the ECMAScript specification, nothing confined to browsers, it’s just about how JavaScript has been defined, and how it’s been working for 20+ years!

… even Babel?

Oh for gosh sake, yes!

OK … so, is there anything safe out there then?

There could be, but because nobody is paying enough attention to this underrated security issue with the most deployed programming language out there, the short answer is no, but we could do better, secure more our environment, just trust our code a bit more, and all it takes is 4 lines of code, at least as the starting point. 🌈

Function traps mitigation

I’ve mentioned this technique more than 10 years ago already, and the gist is that we can trap the most primitive utilities to invoke anything so that nothing can interfere at runtime:

const {apply: a, bind: b, call: c} = Function;
const apply = c.bind(a);
const bind = c.bind(b);
const call = c.bind(c);

We can now trap, as an example, the Symbol.iterator generator of every class we like so that we can invoke it directly to iterate:

const {[Symbol.iterator]: iterator} = Array.prototype;
const iterate = arr => call(iterator, arr);
// example
for (const safe of iterate([1, 2, 3]));

We can now iterate([literally, any, array, value]) and be 100% sure nothing can intercept the passed argument, as long as our traps exist before any possible malicious code. Luckily enough, ads-based scripts are clunky and heavy and, for this reason, rarely loaded as first blocking scripts, especially thanks to search engines’ ranking, a metric that penalizes these kinds of heavy, slow, and blocking scripts.

There are, however, virtually no guarantees that our code runs before any other, especially in this world where everyone can’t wait to jump in the latest tool/bundler, and pretty much nobody has any idea what lands in production, once the whole thing produces an output … well done us! 🤝

Securing just about anything

If we understood how iterate(array) works, we can explore also the usage of bind to secure any method through its owner:

// defining literals is always safe 🍻
// and so is the access to own properties
const obj = {
name: 'safe',
method() {
console.log(this.name);
}
};
const method = bind(obj.method, obj);
method(); // "safe"

The same can be done once and forever, for also public utilities:

const assign = bind(Object.assign, Object);
const entries = bind(Object.entries, Object);

Although, we can imagine doing this for every bloody global utility we need might be very time-consuming, which is why I’ve created proxy-pants, a tree-shaking friendly module full of rare-to-common use cases to bind methods, or accessors, all at once, and in a secure way.

import {bound} from 'proxy-pants';const {
assign,
entries,
defineProperty,
getPrototypeOf
} = bound(Object);

Is it clear? We pass any instance or global utility, and from now on all we need to do is to use assign({}, a, b) or entries(ref) directly, without ever being worried about malicious code out there. 😇

Wait, are you asking about accessors? OK then, here is an example:

import {accessor} from 'proxy-pants';

const {textContent} = accessor(document.body);

// get the current body text
textContent();

// set the new one
textContent('proxy pants!');

A caller or an applier? Sure thing!

import {applier, caller} from 'proxy-pants';

const {hasOwnProperty, toString} = caller(Object.prototype);

hasOwnProperty({any: 'object'}, 'any'); // true
toString(null); // [object Null]

const {fromCharCode} = applier(String);
const charCodes = (...args) => fromCharCode(String, args);
charCodes(60, 61, 62); // <=>

TL;DR I have written already various libraries and helpers around this “secured and trusted execution” and proxy-pants includes these techniques, because we have battle-tested these techniques work in the wild, and our users can sleep hopefully better, surely more secure, so please feel free to use this library too, if you’d like to secure at least most critical parts of your code 😉

🎯 Action Point

I honestly wish nobody should write “upside-down code” such as [...call(iterator, arr)], or even [...iterate(arr)], instead of a natural [...arr] operation, and the good thing is that likely nobody needs to refactor anything or change their code-base, or drop TypeScript, or Dart, to be able to control the security level of their application, except maybe core libraries authors, or libraries around security, passwords, encryption, and so on. Who knows and follows me already, shouldn’t be surprised by the fact I write pretty much every kind of module from scratch, and in this very same post you can find a few reasons I do that: Web developers rarely take security concerns super serious, but while many are often bullying people still writing vanilla JS, instead of using whatever indirection we have these days, I would like to simply bring under your attention that people creating transpilers for your super-cool-secure code that targets JS, also don’t usually pay enough attention to these details, so that any code of yours running out there, is potentially vulnerable to all the attacks I have described so far in this post.

Last but not least: Performance!

A slightly slower code on the critical path, when it comes to increased security, should never be a deciding factor, but that being said, it’s very hard to measure differences between [...call(iterator, arr)] and [...arr], where the latter needs to resolve its prototype and iterator, while the former is a well-known reference in the scope, which needs no prototypal resolution. 🤘

… but also …

You are free to be in complete denial of everything I’ve written in here, especially because I’ve dared to touch one of the most painful points of TypeScript, or Dart, which is lack of control over transpilation targets, but this whole post goal is to make you aware that these kinds of attacks exist, also or especially with transpiled code, so you better understand how to mitigate these, and you can probably do that with both TS or Dart already 🥂

As summary

Just code what you like, do what you love, but if security is a concern, be aware there are patterns to prevent runtime libraries to screw your product, and remember I’ve written already so many solutions, and I’d be more than happy to help you out securing more that specific bit you care about, leaving the rest hopefully not leaky or insecure if that makes sense. 👋

Thank you for reading.

More content at plainenglish.io. Sign up for our free weekly newsletter here.

--

--

Web, Mobile, IoT, and all JS things since 00's. Formerly JS engineer at @nokia, @facebook, @twitter.