@agoric/make-hardener
Advanced tools
Comparing version 0.0.2 to 0.0.3
42
index.js
@@ -22,8 +22,9 @@ // Adapted from SES/Caja - Copyright (C) 2011 Google Inc. | ||
function makeHardener(...initialRoots) { | ||
function makeHardener(initialFringe) { | ||
const { freeze, getOwnPropertyDescriptors, getPrototypeOf } = Object; | ||
const { ownKeys } = Reflect; | ||
// Objects that we won't freeze, either because we've frozen them already, | ||
// or they were one of the initial roots (terminals) | ||
const rootSet = new WeakSet(initialRoots); | ||
// or they were one of the initial roots (terminals). These objects form | ||
// the "fringe" of the hardened object graph. | ||
const fringeSet = new WeakSet(initialFringe); | ||
@@ -47,3 +48,3 @@ function harden(root) { | ||
} | ||
if (rootSet.has(val) || toFreeze.has(val)) { | ||
if (fringeSet.has(val) || toFreeze.has(val)) { | ||
// Ignore if this is an exit, or we've already visited it | ||
@@ -82,2 +83,3 @@ return; | ||
ownKeys(descs).forEach(name => { | ||
const pathname = `${path}.${String(name)}`; | ||
// todo uncurried form | ||
@@ -94,6 +96,6 @@ // todo: getOwnPropertyDescriptors is guaranteed to return well-formed | ||
// todo uncurried form | ||
enqueue(desc.value, `${path}.${name}`); | ||
enqueue(desc.value, `${pathname}`); | ||
} else { | ||
enqueue(desc.get, `${path}.${name}(get)`); | ||
enqueue(desc.set, `${path}.${name}(set)`); | ||
enqueue(desc.get, `${pathname}(get)`); | ||
enqueue(desc.set, `${pathname}(set)`); | ||
} | ||
@@ -108,16 +110,8 @@ }); | ||
function commit() { | ||
// todo curried forEach | ||
// we capture the real WeakSet.prototype.add above, in case someone | ||
// changes it. The two-argument form of forEach passes the second | ||
// argument as the 'this' binding, so we add to the correct set. | ||
toFreeze.forEach(rootSet.add, rootSet); | ||
} | ||
function checkPrototypes() { | ||
prototypes.forEach((path, p) => { | ||
if (!rootSet.has(p)) { | ||
if (!(toFreeze.has(p) || fringeSet.has(p))) { | ||
// all reachable properties have already been frozen by this point | ||
throw new TypeError( | ||
`prototype ${p} of ${path} is not already in the rootSet`, | ||
`prototype ${p} of ${path} is not already in the fringeSet`, | ||
); | ||
@@ -128,9 +122,17 @@ } | ||
function commit() { | ||
// todo curried forEach | ||
// we capture the real WeakSet.prototype.add above, in case someone | ||
// changes it. The two-argument form of forEach passes the second | ||
// argument as the 'this' binding, so we add to the correct set. | ||
toFreeze.forEach(fringeSet.add, fringeSet); | ||
} | ||
enqueue(root); | ||
dequeue(); | ||
// console.log("rootSet", rootSet); | ||
// console.log("fringeSet", fringeSet); | ||
// console.log("prototype set:", prototypes); | ||
// console.log("toFreeze set:", toFreeze); | ||
checkPrototypes(); | ||
commit(); | ||
checkPrototypes(); | ||
@@ -143,2 +145,2 @@ return root; | ||
module.exports = { makeHardener }; | ||
module.exports = makeHardener; |
{ | ||
"name": "@agoric/make-hardener", | ||
"version": "0.0.2", | ||
"version": "0.0.3", | ||
"description": "Create a 'hardener' which freezes the API surface of a set of objects", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
107
README.md
@@ -10,109 +10,14 @@ # MakeHardener | ||
A "Hardened" object is one which is safe to pass to untrusted code: it offers that code an API which can be invoked, but does not allow that code to modify the internals of the object or anything it depends upon. For example, a simple-but-insecure way to offer an increment-only counter API to some users might be as follows: | ||
## How to use | ||
```js | ||
function makeCounterSet() { | ||
let counters = new Map(); | ||
const API = { | ||
increment(name): { | ||
if (!counters.has(name)) { | ||
counters.set(name, 0); | ||
} | ||
const newValue = counters.get(name) + 1; | ||
counters.set(name, newValue); | ||
return newValue; | ||
} | ||
return API; | ||
} | ||
> Note: If you're writing an application, you probably don't want to use this package directly. You'll want to use the `harden()` function provided in [SES](https://github.com/Agoric/SES) to perform an all-encompassing "deep freeze" on your objects. Alternatively, if you want to test your code before using it in SES, you can import the [@agoric/harden package](https://github.com/Agoric/Harden). Note that without SES, `harden()` is insecure, and should be used for testing purposes only. | ||
const newAPI = makeCounterSet(); | ||
untrustedUser1(newAPI); | ||
untrustedUser2(newAPI); | ||
``` | ||
## Why do we need to "harden" objects? | ||
Now, what could our `untrustedUser` do that could violate the increment-only property of these counters? | ||
Please see the [harden()](https://github.com/Agoric/Harden) package for more documentation. | ||
### Break Functionality For Other Users | ||
## Creating a custom harden() Function | ||
```js | ||
function untrustedUser1(newAPI) { | ||
delete newAPI.increment; | ||
} | ||
``` | ||
This package (`@agoric/make-hardener`) provides a `makeHardener()` which can be used to build your own `harden()` function. When you call `makeHardener()`, you give it an iterable of stopping points (the "fringe"), and the recursive property walk will stop its search when it runs into the fringe. The resulting `harden()` will throw an exception if anything it freezes has a prototype that is not already in the fringe, or was frozen during the same call (and thus added to the fringe). | ||
That would prevent anyone from using the counter at all. | ||
### Snoop on Usage By Other Users | ||
```js | ||
function untrustedUser1(newAPI) { | ||
const origIncrement = newAPI.increment; | ||
const otherNames = new Set(); | ||
newAPI.increment = function(name) { | ||
otherNames.add(name); | ||
return origIncrement(name); | ||
}; | ||
} | ||
``` | ||
This lets one user learn the names being used by other user. | ||
### Violate the API Contract | ||
```js | ||
function untrustedUser1(newAPI) { | ||
Map.set = function(name, value) {}; | ||
} | ||
``` | ||
This changes the `Map` which our counter API relies upon: when it tries to update the value, the update is ignored, so the counter will stay at 0 forever. | ||
As a side-effect, it breaks `Map` for everyone in that Realm (which generally means everyone in the same process). This is pretty drastic, but you can imagine a situation where the target object was the only user of some shared utility, and the attacker could selectively modify the utility to affect some users without affecting others. For example, `Map.set` might look at the name and only ignore updates for specific ones. | ||
### Modify Prototypes to Violate the API Contract | ||
Our example object inherits directly from `Object.prototype`, but a more complex program might create intermediate objects and use them as prototypes to share behavior between multiple instances. These intermediate objects are vulnerable too: | ||
```js | ||
function untrustedUser1(newAPI) { | ||
Object.getPrototypeOf(newAPI).something = function(arg) {}; | ||
} | ||
``` | ||
We need to protect against this too. | ||
## Preventing API Misuse by Freezing | ||
`Object.freeze()` was created to prevent exactly this sort of misbehavior. Once an object is frozen, its properties cannot be changed (new ones cannot be added, and existing ones cannot be modified or removed). This prevents the most basic attacks: | ||
```js | ||
const newAPI = makeCounterSet(); | ||
Object.freeze(newAPI); | ||
untrustedUser1(newAPI); | ||
untrustedUser2(newAPI); | ||
``` | ||
However the API object might expose properties that point to other API objects, and Object.freeze() only protects its single argument. We want to traverse all exposed properties of our API object and freeze them too, recursively. We want to make sure the prototype chain is protected too, as well as any utilities that our API depends upon (like `Map`). | ||
`harden()` is a function which performs this recursive freezing of an API surface, preventing all of the attacks described above: | ||
```js | ||
const newAPI = harden(makeCounterSet()); | ||
untrustedUser1(newAPI); | ||
untrustedUser2(newAPI); | ||
``` | ||
[SES](https://github.com/Agoric/SES) is a programming environment in which all the "primordials" (the built-in Javascript objects like `Map`, `Number`, `Array`, and so on) are frozen. In a SES environment, simply `harden()` the objects that you give to other code to interact with them safely, according to the API that you've constructed. | ||
## Creating a harden() Function | ||
This package (`@agoric/make-hardener`) provides a `makeHardener()` which can be used to build your own `harden()` function. `makeHardener` is "pure", meaning that it does not know about any specific primordials. When you call `makeHardener()`, you give it a set of stopping points, and the recursive property walk will stop its search when it runs into one of these points. The resulting `harden()` will throw an exception if anything it freezes has a prototype that is not already in the set of stopping points (or was frozen during the same call). | ||
There is a related package named `@agoric/harden` that uses `makeHardener` to provide a `harden()` that is a "resource module": it has some authority baked in. `@agoric/harden` could be used as a communication channel between two unrelated pieces of code, by testing whether a prearranged object is already frozen or not (TODO: how exactly?). It is also tied to a specific list of primordials, making it less useful for an environment like SES that needs to specify its own list. | ||
[travis-svg]: https://travis-ci.com/Agoric/MakeHardener.svg?branch=master | ||
@@ -119,0 +24,0 @@ [travis-url]: https://travis-ci.com/Agoric/MakeHardener |
const test = require('tape'); | ||
const { makeHardener } = require('../index.js'); | ||
const makeHardener = require('../index.js'); | ||
test('makeHardener', t => { | ||
const h = makeHardener(Object.prototype); | ||
const h = makeHardener([Object.prototype]); | ||
const o = { a: {} }; | ||
@@ -15,3 +15,3 @@ t.equal(h(o), o); | ||
const parent = { I_AM_YOUR: 'parent' }; | ||
const h = makeHardener(parent, Object.prototype); | ||
const h = makeHardener([parent, Object.prototype]); | ||
const o = { a: {} }; | ||
@@ -29,4 +29,4 @@ Object.setPrototypeOf(o, parent); | ||
// at least one prototype is missing in each hardener | ||
const h1 = makeHardener(Object.prototype); | ||
const h2 = makeHardener(parent); | ||
const h1 = makeHardener([Object.prototype]); | ||
const h2 = makeHardener([parent]); | ||
const o = { a: {} }; | ||
@@ -44,3 +44,3 @@ Object.setPrototypeOf(o, parent); | ||
test('harden the same thing twice', t => { | ||
const h = makeHardener(Object.prototype); | ||
const h = makeHardener([Object.prototype]); | ||
const o = { a: {} }; | ||
@@ -55,3 +55,3 @@ t.equal(h(o), o); | ||
test('harden objects with cycles', t => { | ||
const h = makeHardener(Object.prototype); | ||
const h = makeHardener([Object.prototype]); | ||
const o = { a: {} }; | ||
@@ -66,3 +66,3 @@ o.a.foo = o; | ||
test('harden overlapping objects', t => { | ||
const h = makeHardener(Object.prototype); | ||
const h = makeHardener([Object.prototype]); | ||
const o1 = { a: {} }; | ||
@@ -78,1 +78,31 @@ const o2 = { a: o1.a }; | ||
}); | ||
test('do not commit early', t => { | ||
// refs #4 | ||
const h = makeHardener([Object.prototype]); | ||
const a = { a: 1 }; | ||
const b = { b: 1, __proto__: a }; | ||
const c = { c: 1, __proto__: b }; | ||
t.throws(() => h(b), TypeError); | ||
// the bug is that 'b' is marked as hardened. If that happens, harden(c) | ||
// will pass when it was supposed to throw. | ||
t.throws(() => h(c), TypeError); | ||
t.end(); | ||
}); | ||
test('can harden all objects in a single call', t => { | ||
// refs #4 | ||
const h = makeHardener([Object.prototype, Object.getPrototypeOf([])]); | ||
const a = { a: 1 }; | ||
const b = { b: 1, __proto__: a }; | ||
const c = { c: 1, __proto__: b }; | ||
h([a, b, c]); | ||
t.ok(Object.isFrozen(a)); | ||
t.ok(Object.isFrozen(b)); | ||
t.ok(Object.isFrozen(c)); | ||
t.end(); | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
229
23061
30