Hardened JavaScript
What is Hardened JavaScript?
In short, JavaScript provides a highly malleable scripting environment suitable for running programs written by strangers on behalf of a user with limited access to the user's resource.
If that program chooses to sacrifice some of that malleability, they gain in exchange, the ability to safely invite other programs to interact with them directly at the boundary between individual objects.
That is Hardened JavaScript.
Software has a long tradition of increasing interactivity by sending programs from servers to clients-from the VT 100 terminal to the web browser, even arguably the pattern of installing any network software, all increase interactivity.
They increase interactivity by eliminating expensive round trips between the user and the service.
They risk increasing vulnerability because the user must be able to run arbitrary programs from strangers on the internet.
More so if the programming languages Turing Complete.
We aim to create highly interactive software while avoiding vulnerability.
To have interactivity, we need to be able to run other people's programs, but running other people's programs is dangerous and people will even tell you that you shouldn't.
But I am here to tell you that you can run other people's programs safely.
The solution is Hardened JavaScript.
In the Odyssey Odysseus encounters the sirens, dangerous creatures who lure sailors with their song only to dash their ships against their rocky shore.
Odysseus commands his crew to stuff their ears and bind him to the mast of the galley intent upon hearing the song and not suffering the consequences so he could interact without vulnerability.
This was of course, a classical metaphor for running strangers programs off the internet.
The siren's song is akin to a program that you would very much like to run with the ability to delight your senses and intellect, but without direct access to your motor functions, But why JavaScript?
It is no accident that we rely on JavaScript to run arbitrary programs off the internet.
The language is designed to run in a sandbox.
Web browsers are user agents.
They mediate interaction between the programs provided by strangers and their access to the user's resources.
JavaScript mitigates vulnerability when there are only two parties, the stranger and the user.
This interaction model works with the sandbox because the stranger's program is welcome to ruin the sandbox for itself and cannot wreak havoc beyond the boundary that exists between the user agent and the isolated program.
However.
The notion that there are only two parties involved in a webpage is a rapidly deteriorating fiction.
Every end of a modern web application, even a centralized monolith, mixes conflicting motivations of myriad agents-the user, the service, their advertisements, their vendors and their attackers.
And while we might find some comfort in the security boundaries that are firewalls or authenticated asynchronous communication over encrypted connections, those boundaries will not protect us from the dependencies we entrain from our favorite package management system.
When there are three or more parties interacting in a sandbox, a sandbox is not sufficient.
When one stranger can pollute the environment, the other stranger becomes vulnerable and the strangers can attack each other directly at the user's expense.
For three or more parties to interact we need a solid foundation, not a box of sand.
And in a world where we can safely create interactions involving any number of strangers.
New categories of software become possible or existing categories become safe.
Though JavaScript was not originally intended to sandbox multiple potentially adversarial programs simultaneously, it has always had some convenient properties.
And about 10 years ago, it gained the last of the remaining properties necessary to safely evaluate strangers code without expensive static analysis or rewriting.
A JavaScript program can't wander around in memory, looking for pointers to powerful objects, and it can't invoke kernel functions without calling a host function first.
That means the program can be denied host functions outright and can be delegated specific and revocable pointers to objects we wish it to be able to use.
A JavaScript function can hold a powerful object in local scope and use it on behalf of callers without sharing all of that power.
The run to completion event loop model, as opposed to shared memory concurrency like threads for one avoids the hazard of deadlock, which threatens liveliness and also gives programs the ability to ensure that some functions can be called in separate events, avoiding re-entrance hazards.
JavaScript was not born suitable for co-tenant programs, but strict mode eliminates the most pernicious misfeatures like the arguments object, which reveals dynamic scope and with blocks and with strict mode JavaScript gains the ability to harden objects, making them irrevocably tamper-proof.
Imagine you serve data and want to allow clients to be able to make arbitrary queries on that data without transmitting all of the data.
You arrange for a client to send a program to the data, instead of the data to the program.
We typically use hobbled programming languages for this kind of interaction to avoid making the surface vulnerable to these programs.
In JavaScript, you might naively evaluate an arbitrary JavaScript query, but with the soft and malleable version of the JavaScript language, the quierier receives far too much power.
It can read database from scopr.
It can call any method of any object in scope.
It can modify any mutable variable ion scope.
It can install a thunk on the prototype of a popular method.
It can add a proxy to the prototype of the global object.
It can load your powerful networking module and exfiltrate your secret burger sauce recipe.
In JavaScript you run other people's programs using an evaluator.
Contrary to popular wisdom.
eval is not evil, and I can prove it.
The Levinshtein distance between evil and evil is not zero.
Even if you give it a discount for vowel substitution.
But like evil, eval comes in many forms.
The oldest and most perilous form is direct eval, where the program you run inherits the caller's scope.
This is the so-called dynamic scoping eval.
And it can do arcane things like introduce variables to the caller's scope.
This eval would happily allow the sirens to overshadow undefined in the scope of the caller.
The lesser eval is indirect eval.
Indirect eval is a minor reformation of the direct eval that represents the program and reparents the program in global scope.
Although the only way to invoke a direct eval is to call a function literally named eval also happens to be bound to the original eval function in global scope, indirect eval works by calling the original global eval function any other way.
In these two cases, we're running the siren's song that makes perfectly valid statements like "two plus two is math" and "array is object".
The subtle eval is the function constructor, which compiles a program and runs it in a closure scope parented on the global scope.
In this case, the siren song sets NAN to a very special number, just like the real NANs..
Eval can be subverted in many ways, mostly by exploiting the pervasive mutability of the JavaScript environment.
The web has benefited tremendously from that pervasive mutability that it gives programs.
It is this malleability that has allowed JavaScript to grow as a language.
Notably shims are programs that anticipate new features and patch them into the global scope.
Now we can turn that pervasive mutability in upon itself, taming and hardening the runtime environment, making it suitable for multi-tenant programs.
Hardened JavaScript like Gaul before it is divided into three parts-lockdown, harden and compartment.
Lockdown prepares, Hardened defends, and compartments isolates.
With these three devices, programs are not automatically safe, but have ground to stand on to defend their own design.
Lockdown prepares the shared the JavaScript primordial objects like array and object constructors, prototypes, fixes some features that would allow programs to watch or interfere with one another, removes unrecognized methods from these objects just in case, and then freezes them.
Some of these objects are subtle, like the prototypes for various iterators or the async function prototype, which any program can find.
But not just by visiting all the properties of the global object.
The harden function freezes an object and its transitive properties and prototype, rendering it, it's prototype, and anything else reachable from it surface tamper-proof.
Lockdown reveals the Hardened function so that programs can safely share their interfaces with, with strangers.
Lockdown also prepares a compartment constructor that can run arbitrary programs in an environment that has a unique global this eval function and compartment constructor.
These can evaluate programs with the, with only the capabilities they have been explicitly granted.
The host environment still has access to lots of powerful objects, maybe even some powerful modules, and it can delegate these powers to child compartments.
Within a compartment, prototype pollution attacks through the shared intrinsics are not possible.
And the harden function is available for programs to prevent prototype pollution on their own objects.
You can't subvert the definition of NAN.
You can't redefine Math.
You can't munge the shared prototypes.
You only get what the host gave you.
So returning to our concrete example, we can provide a search feature that can run arbitrary queries.
The application arranges to call lockdown once as early as possible in the program's life, because lockdown itself is vulnerable to all the code that runs before it.
The application then arranges for a compartment in which it can run queries.
Because this compartment will be shared by multiple parties and contains no modules, we can freeze the compartment's global this.
Then because we are able to inject individual items into the scope of the query, we capture a copy of the safe function constructor from within the compartment and use that to compile queries.
In addition, we use harden to make the search function and the arrays it returns tamper-proof.
With this arrangement queries cannot attack the database directly because they do not have access to the database object.
They also cannot stage a man in the middle attack or exploit re-entrance by polluting the shared array prototype.
And they can't reach the file system, much less grief it or worse.
They do not have access to powerful modules or other forms of what we call, ambient authority, powerful objects laying around for any part of your program to use.
The most common approach to creating sandboxes relies on a coarser boundary called a 'realm'.
Like a site safe origin, same origin frame or what NodeJS and V8 call a 'VM context' with these approaches each tenant program gets their own unique set of primordial's like their own array.
The approach is fraught with a number of inconveniences, but most notably identity discontinuity, the array from one realm is chemically incompatible with the array of another.
To address this properly, you need a boundary, a layer of objects that ensures that the blue arrays are seen only on the left of the membrane, and the yellow arrays are only seen on the right.
That boundary might be serialization and deserialization of messages.
And it might have to be asynchronous.
It might be a membrane or a layer of proxies that ensure from objects from different spaces and times don't meet each other.
But with lockdown, the host and guest code can stand on the same foundation safely.
And ultimately, while separate realms can defend explicitly partitioned programs, you may find that the enemy is already within the gate.
All modern software runs in a crowded house, regardless of whether they can do so safely.
All programs are vulnerable to their dependencies and their so-called software supply chain.
A modern JavaScript program consists of maybe 3% novel code and 97% dependencies that must be kept up to date.
All of which have an opportunity and interfere with the developer's plans.
And nearly every programming language, especially pliable JavaScript these dependencies run with all of the same power as the 3% that orchestrates the whole.
And if that does not give you pause, consider that engines like NodeJS implement most of their powerful API's in the same realm as the programs that they run.
Our partners at Metamask built a tool called LavaMoat on top of Hardened JavaScript that allows them to limit the attack surface they expose to third-party dependencies all the way from their front end to their build tooling during development.
Together, we're building a tool called 'endo' that demonstrates how Hardened JavaScript including it's a synchronous compartment based module system can be used to host many applications designed without foreknowledge of Hardened JavaScript.
We at Agoric and members of the SaaS community are pursuing standardization of the Hardened JavaScript proposals, but you need not wait.
We have built a shim that implements lockdown, harden and compartment with very high fidelity.
And if you're targeting embedded systems, particularly Moddable's, XS JavaScript engine implements, these features natively.
For Agoric Hardened JavaScript is the foundation that allows us to safely and deterministically runs smart contracts.
To us, Hardened JavaScript is part of a decentralized operating system where we use promises as proxies for remote objects, eschewing heavy RPC frameworks in favor of asynchronous message passing between Hardened objects, where we can arrive at consensus through a replicated log of these messages, and deterministic JavaScript replay.
With Moddable's XS JavaScript engine we can even snapshot running programs and resume them later, not even necessarily on the same host.
But why Hardened JavaScript?
Allow me to digress into my own motivation for contributing to this project.
I like exciting projects.
To me, an exciting project has three noteworthy traits.
First it is evident that if the project succeeds, almost everything naturally comes to depend on it, and the new world is bigger, more cooperative, safer or accessible than the old one.
The project would cause a Cambrian explosion of diversity and activity.
Second, nobody wants it.
Most people will simply not know about it.
It's not on their radar.
Some people will know about it and dismiss it as unlikely to gain traction.
Some people will resist it because it's inconvenient since it will require rethinking or retooling, all of which are fair.
And third, if you work on the project a little every day, it gradually makes its way up to some invisible watershed and shifts from being radical to inevitable.
By way of example, if I may digress further into the dangerous territory of conceit, I've enjoyed some exciting projects.
In 2006, JavaScript didn't have a module system.
I started promoting a prototype in an informal standard in 2008.
People at that time would say 'JavaScript doesn't need a module system'.
Manually topo-sorting script tags has always worked, and it always will.
And manually sorting script tags was adequate for all of the software made at that time in no small part, because it was the only kind of software you could make.
But it's not obvious on the near side of a hill what lays on the far.
In 2010, I worked with a new group called CommonJS, and we agreed on a standard for sharing modules that transcended the walled gardens of the warring toolkits of the time.
Then NodeJS picked it up to bootstrap its own ecosystem.
And now we write a lot more kinds of software than we did with hand sorted script tags.
But if getting the JavaScript community to share modules was a pitched battle with long odds, even though modules were a solved problem in most languages and nearly everyone knew what they were and how they were supposed to work, you can only imagine how exciting it was to be working to make promises palatable to JavaScript developers.
Of course, I've poured my soul into hundreds of projects that never saw the light of day, but, based on my experience with JavaScript modules and promises, I think you're likely to hear about Hardened JavaScript again.
The purpose of Hardened JavaScript is to realize this Cambrian increase in software diversity by allowing a greater degree of cooperation between programs.
So please sail forth from here, listen to the siren song and live to tell the tale.
I'm Kris Kowl an engineer with the good fortune of working with the incredible team at Agoric.
And you can find the Hardened JavaScript shim in the endo project repository on GitHub, or by simply installing ses.