Skip to content

Commit

Permalink
fix(resolve): protect against reentrancy attack (#401)
Browse files Browse the repository at this point in the history
* fix(resolve): protect against reentrancy attack

Closes #9

* fix(resolve): harden argument to HandledPromise.resolve

The harden calls in eventual-send already need an SES-like
environment for proper security.  Make HandledPromise.resolve
simpler and prevent proxy trickery.

* fix(HandledPromise): set prototype to Promise

* fix(test-thenable): proper workaround of override mistake
  • Loading branch information
michaelfig authored Jan 27, 2020
1 parent f3a760b commit d1f25ef
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 17 deletions.
5 changes: 5 additions & 0 deletions packages/eventual-send/changelogs/9.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
* `HandledPromise.resolve(p)` (and therefore `E.resolve(p)`) now
assimilate Promises that have been mutated to add a `.then` method.
These are delayed to a future turn before calling their `.then` method.
This protects against reentrancy attacks, but only when running
under SES.
2 changes: 1 addition & 1 deletion packages/eventual-send/src/E.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export default function makeE(HandledPromise) {
return true;
},
get(_target, prop) {
return HandledPromise.get(x, prop);
return harden(HandledPromise.get(x, prop));
},
});

Expand Down
53 changes: 37 additions & 16 deletions packages/eventual-send/src/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
/* global HandledPromise */
/* global HandledPromise SES */

import harden from '@agoric/harden';

import makeE from './E';

const { defineProperties, getOwnPropertyDescriptors } = Object;
const {
defineProperties,
getOwnPropertyDescriptors,
getOwnPropertyDescriptor: gopd,
getPrototypeOf,
isFrozen,
} = Object;

const { prototype: promiseProto } = Promise;
const { then: originalThen } = promiseProto;

// 'E' and 'HandledPromise' are exports of the module

Expand Down Expand Up @@ -56,7 +65,7 @@ export function makeHandledPromise(Promise) {
// handled Promises to their corresponding fulfilledHandler.
let forwardingHandler;
let handle;
let handledPromiseResolve;
let promiseResolve;

function HandledPromise(executor, unfulfilledHandler = undefined) {
if (new.target === undefined) {
Expand Down Expand Up @@ -224,12 +233,18 @@ export function makeHandledPromise(Promise) {
return handledP;
}

HandledPromise.prototype = Promise.prototype;
HandledPromise.prototype = promiseProto;
Object.setPrototypeOf(HandledPromise, Promise);

// Uncomment this line if needed for conformance to the proposal.
// Currently the proposal does not specify this, but we might change
// our mind.
// Object.setPrototypeOf(HandledPromise, Promise);
function isFrozenPromiseThen(p) {
return (
isFrozen(p) &&
getPrototypeOf(p) === promiseProto &&
promiseResolve(p) === p &&
gopd(p, 'then') === undefined &&
gopd(promiseProto, 'then').value === originalThen // unnecessary under SES
);
}

const staticMethods = harden({
get(target, key) {
Expand All @@ -253,15 +268,21 @@ export function makeHandledPromise(Promise) {
resolve(value) {
ensureMaps();
// Resolving a Presence returns the pre-registered handled promise.
const handledPromise = presenceToPromise.get(value);
if (handledPromise) {
return handledPromise;
let resolvedPromise = presenceToPromise.get(value);
if (!resolvedPromise) {
resolvedPromise = promiseResolve(value);
}
const basePromise = Promise.resolve(value);
if (basePromise === value) {
return value;
// Prevent any proxy trickery.
harden(resolvedPromise);
if (isFrozenPromiseThen(resolvedPromise)) {
return resolvedPromise;
}
return handledPromiseResolve(value);
// Assimilate the thenable.
const executeThen = (resolve, reject) =>
resolvedPromise.then(resolve, reject);
return harden(
promiseResolve().then(_ => new HandledPromise(executeThen)),
);
},
});

Expand Down Expand Up @@ -340,6 +361,6 @@ export function makeHandledPromise(Promise) {
return new HandledPromise(executor);
};

handledPromiseResolve = Promise.resolve.bind(HandledPromise);
promiseResolve = Promise.resolve.bind(Promise);
return harden(HandledPromise);
}
34 changes: 34 additions & 0 deletions packages/eventual-send/test/test-thenable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import test from 'tape-promise/tape';
import { E, HandledPromise } from '../src/index';

test('E.resolve is always asynchronous', async t => {
try {
const p = new Promise(_ => {});
// Work around the override mistake on SES.
Object.defineProperty(p, 'then', { value: (res, _rej) => res('done') });
let thened = false;
const p2 = E.resolve(p).then(ret => (thened = ret));
t.is(thened, false, `p2 is not yet resolved`);
t.equal(await p2, 'done', `p2 is resolved`);
} catch (e) {
t.isNot(e, e, 'unexpected exception');
} finally {
t.end();
}
});

test('HandledPromise.resolve is always asynchronous', async t => {
try {
const p = new Promise(_ => {});
// Work around the override mistake on SES.
Object.defineProperty(p, 'then', { value: (res, _rej) => res('done') });
let thened = false;
const p2 = HandledPromise.resolve(p).then(ret => (thened = ret));
t.is(thened, false, `p2 is not yet resolved`);
t.equal(await p2, 'done', `p2 is resolved`);
} catch (e) {
t.isNot(e, e, 'unexpected exception');
} finally {
t.end();
}
});

0 comments on commit d1f25ef

Please sign in to comment.