Scope handler inherits from throwinghandler
MODIFY
shim/src/evaluators.js
shim/src/evaluators.js
@@ -16,7 +16,7 @@ import {
setPrototypeOf,
stringIncludes
} from './commons';
-import { ScopeHandler } from './scopeHandler';
+import { createScopeHandler } from './scopeHandler';
import { rejectImportExpressions } from './block-imports';
import { assert, throwTantrum } from './utilities';
@@ -113,7 +113,7 @@ function createScopedEvaluatorFactory(unsafeRec, constants) {
export function createSafeEvaluatorFactory(unsafeRec, safeGlobal) {
const { unsafeFunction } = unsafeRec;
- const scopeHandler = new ScopeHandler(unsafeRec);
+ const scopeHandler = createScopeHandler(unsafeRec);
const optimizableGlobals = getOptimizableGlobals(safeGlobal);
const scopedEvaluatorFactory = createScopedEvaluatorFactory(unsafeRec, optimizableGlobals);
@@ -138,7 +138,7 @@ export function createSafeEvaluatorFactory(unsafeRec, safeGlobal) {
eval(src) {
src = `${src}`;
rejectImportExpressions(src);
- scopeHandler.useUnsafeEvaluator = true;
+ scopeHandler.allowUnsafeEvaluatorOnce();
let err;
try {
// Ensure that "this" resolves to the safe global.
@@ -149,10 +149,9 @@ export function createSafeEvaluatorFactory(unsafeRec, safeGlobal) {
throw e;
} finally {
// belt and suspenders: the proxy switches this off immediately after
- // the first access, but just in case we clear it here too
- if (scopeHandler.useUnsafeEvaluator !== false) {
- scopeHandler.useUnsafeEvaluator = false;
- throwTantrum('handler sets useUnsafeEvaluator = false', err);
+ // the first access, but if that's not the case we abort.
+ if (scopeHandler.unsafeEvaluatorAllowed()) {
+ throwTantrum('handler did not revoke useUnsafeEvaluator', err);
}
}
}

MODIFY
shim/src/scopeHandler.js
shim/src/scopeHandler.js
-// the ScopeHandler manages a Proxy which serves as the global scope for the
-// safeEvaluator operation (the Proxy is the argument of a 'with' binding).
-// As described in createSafeEvaluator(), it has several functions:
-// * allow the very first (and only the very first) use of 'eval' to map to
-// the real (unsafe) eval function, so it acts as a 'direct eval' and can
-// access its lexical scope (which maps to the 'with' binding, which the
-// ScopeHandler also controls)
-// * ensure that all subsequent uses of 'eval' map to the safeEvaluator,
-// which lives as the 'eval' property of the safeGlobal
-// * route all other property lookups at the safeGlobal
-// * hide the unsafeGlobal which lives on the scope chain above the 'with'
-// * ensure the Proxy invariants despite some global properties being frozen
-
-import { getPrototypeOf, objectHasOwnProperty } from './commons';
-
-export class ScopeHandler {
- // Properties stored on the handler are not available from the proxy.
-
- // the Proxy is only used by with(), so the Handler only needs to implement
- // a few properties: has, get, set (which we leave at the default)
-
- // todo: throw if any traps other than get/set/has are run (e.g.
- // getOwnPropertyDescriptors, apply, getPrototypeOf) . Make this handler
- // inherit from a second one whose 'get' property always throws.
-
- constructor(unsafeRec) {
- this.unsafeGlobal = unsafeRec.unsafeGlobal;
- this.unsafeEval = unsafeRec.unsafeEval;
-
- // this flag allow us to determine if the eval() call is a controlled
- // eval done by the realm's code or if it is user-land invocation, so
- // we can react differently.
- this.useUnsafeEvaluator = false;
-
- // todo: this.shadowTarget = getPrototypeOf(somehow_get_target)
+import { freeze, getPrototypeOf, objectHasOwnProperty } from './commons';
+import { throwTantrum } from './utilities';
+
+/**
+ * alwaysThrowHandler is a proxy handler which throws on any trap called.
+ * It's made from a proxy with a get trap that throws. Its target is
+ * an immutable (frozen) object and is safe to share.
+ */
+const alwaysThrowHandler = new Proxy(freeze({}), {
+ get(target, prop) {
+ throwTantrum(`unexpected scope handler trap called: ${prop}`);
}
+});
+
+/**
+ * ScopeHandler manages a Proxy which serves as the global scope for the
+ * safeEvaluator operation (the Proxy is the argument of a 'with' binding).
+ * As described in createSafeEvaluator(), it has several functions:
+ * - allow the very first (and only the very first) use of 'eval' to map to
+ * the real (unsafe) eval function, so it acts as a 'direct eval' and can
+ * access its lexical scope (which maps to the 'with' binding, which the
+ * ScopeHandler also controls).
+ * - ensure that all subsequent uses of 'eval' map to the safeEvaluator,
+ * which lives as the 'eval' property of the safeGlobal.
+ * - route all other property lookups at the safeGlobal.
+ * - hide the unsafeGlobal which lives on the scope chain above the 'with'.
+ * - ensure the Proxy invariants despite some global properties being frozen.
+ */
+export function createScopeHandler(unsafeRec) {
+ const { unsafeGlobal, unsafeEval } = unsafeRec;
+
+ // This flag allow us to determine if the eval() call is an done by the
+ // realm's code or if it is user-land invocation, so we can react differently.
+ let useUnsafeEvaluator = false;
+
+ // todo (optimization): keeping a reference to the shadow to avoid calling
+ // getPrototypeOf on the target every time the get trap is invoked
+ // const shadowTarget = getPrototypeOf(somehow_get_target)
+
+ return {
+ // The scope handler throws if any trap other than get/set/has are run
+ // (e.g. getOwnPropertyDescriptors, apply, getPrototypeOf).
+ // eslint-disable-next-line no-proto
+ __proto__: alwaysThrowHandler,
+
+ allowUnsafeEvaluatorOnce() {
+ useUnsafeEvaluator = true;
+ },
+
+ unsafeEvaluatorAllowed() {
+ return useUnsafeEvaluator;
+ },
get(target, prop) {
// Special treatment for eval. The very first lookup of 'eval' gets the
@@ -41,10 +57,10 @@ export class ScopeHandler {
// the 'with' context.
if (prop === 'eval') {
// test that it is true rather than merely truthy
- if (this.useUnsafeEvaluator === true) {
- // reset before use
- this.useUnsafeEvaluator = false;
- return this.unsafeEval;
+ if (useUnsafeEvaluator === true) {
+ // revoke before use
+ useUnsafeEvaluator = false;
+ return unsafeEval;
}
return target.eval;
}
@@ -64,15 +80,11 @@ export class ScopeHandler {
}
// Prevent the lookup for other properties.
return undefined;
- }
+ },
// eslint-disable-next-line class-methods-use-this
set(target, prop, value) {
- // Set the value on the shadow. The target itself is an empty
- // object that is only used to prevent a frozen eval property.
- // todo: use this.shadowTarget, for speedup
-
- // new todo: allow modifications when target.hasOwnProperty(prop) and it
+ // todo: allow modifications when target.hasOwnProperty(prop) and it
// is writable, assuming we've already rejected overlap (see
// createSafeEvaluatorFactory.factory). This TypeError gets replaced with
// target[prop] = value
@@ -81,9 +93,10 @@ export class ScopeHandler {
throw new TypeError(`do not modify endowments like ${String(prop)}`);
}
getPrototypeOf(target)[prop] = value;
+
// Return true after successful set.
return true;
- }
+ },
// we need has() to return false for some names to prevent the lookup from
// climbing the scope chain and eventually reaching the unsafeGlobal
@@ -97,9 +110,8 @@ export class ScopeHandler {
// the lack of ReferenceError in the 'typeof' case. Must either reliably
// distinguish these two cases (the trap behavior might be different), or
// we rely on a mandatory source-to-source transform to change 'typeof abc'
- // to XXX. We already need a mandatory parse to prevent the 'import' and
- // 'import.meta' expressions, since they're special forms instead of merely
- // being a global variable
+ // to XXX. We already need a mandatory parse to prevent the 'import',
+ // since it's a special form instead of merely being a global variable/
// note: if we make has() return true always, then we must implement a
// set() trap to avoid subverting the protection of strict mode (it would
@@ -119,10 +131,11 @@ export class ScopeHandler {
}
// hide all properties of unsafeGlobal at the expense of 'typeof' being
// wrong for those properties
- if (prop in this.unsafeGlobal) {
+ if (prop in unsafeGlobal) {
// in browser, 'document = 3', this will add a property to your safeGlobal
return true;
}
return false;
}
+ };
}

ADD
shim/test/realm/scopeHandler.js
+import test from 'tape';
+import sinon from 'sinon';
+import { createScopeHandler } from '../../src/scopeHandler';
+
+test('scope hander traps', t => {
+ t.plan(13);
+
+ sinon.stub(console, 'error').callsFake();
+
+ const handler = createScopeHandler({});
+
+ ['has', 'get', 'set'].forEach(trap => t.doesNotThrow(() => handler[trap]));
+
+ [
+ 'apply',
+ 'construct',
+ 'defineProperty',
+ 'delteProperty',
+ 'getOwnProperty',
+ 'getPrototypeOf',
+ 'isExtensible',
+ 'ownKeys',
+ 'preventExtensions',
+ 'setPrototypeOf'
+ ].forEach(trap => t.throws(() => handler[trap]), /unexpected scope handler trap called/);
+
+ // eslint-disable-next-line no-console
+ console.error.restore();
+});
+
+test('scope hander has', t => {
+ t.plan(9);
+
+ const unsafeGlobal = { foo: {} };
+ const handler = createScopeHandler({ unsafeGlobal });
+ const target = { bar: {} };
+
+ t.equal(handler.has(target, 'eval'), true);
+ handler.allowUnsafeEvaluatorOnce();
+ t.equal(handler.has(target, 'eval'), true);
+ handler.get(target, 'eval'); // trigger the revoke
+ t.equal(handler.has(target, 'eval'), true);
+ t.equal(handler.has(target, 'eval'), true); // repeat
+
+ t.equal(handler.has(target, Symbol.unscopables), false);
+
+ t.equal(handler.has(target, 'arguments'), false);
+ t.equal(handler.has(target, 'foo'), true);
+ t.equal(handler.has(target, 'bar'), true);
+ t.equal(handler.has(target, 'dummy'), false);
+});
+
+test('scope hander get', t => {
+ t.plan(13);
+
+ const unsafeGlobal = { foo: {} };
+ const unsafeEval = {};
+ const handler = createScopeHandler({ unsafeGlobal, unsafeEval });
+ const safeGlobal = { eval: {}, bar: {} };
+ const target = Object.create(safeGlobal);
+
+ t.equal(handler.unsafeEvaluatorAllowed(), false); // initial
+ t.equal(handler.get(target, 'eval'), target.eval);
+
+ handler.allowUnsafeEvaluatorOnce();
+ t.equal(handler.unsafeEvaluatorAllowed(), true);
+ t.equal(handler.get(target, 'eval'), unsafeEval);
+ t.equal(handler.unsafeEvaluatorAllowed(), false);
+ t.equal(handler.get(target, 'eval'), target.eval);
+ t.equal(handler.unsafeEvaluatorAllowed(), false);
+ t.equal(handler.get(target, 'eval'), target.eval); // repeat
+
+ t.equal(handler.get(target, Symbol.unscopables), undefined);
+
+ t.equal(handler.get(target, 'arguments'), undefined);
+ t.equal(handler.get(target, 'foo'), undefined);
+ t.equal(handler.get(target, 'bar'), target.bar);
+ t.equal(handler.get(target, 'dummy'), undefined);
+});
+
+test('scope hander et', t => {
+ t.plan(4);
+
+ const unsafeGlobal = {};
+ const handler = createScopeHandler({ unsafeGlobal });
+ const safeGlobal = { bar: {} };
+ const endowments = { foo: {} };
+ const target = Object.create(safeGlobal, Object.getOwnPropertyDescriptors(endowments));
+
+ const evil = {};
+ handler.set(target, 'eval', evil);
+ t.equal(safeGlobal.eval, evil);
+
+ const bar = {};
+ handler.set(target, 'bar', bar);
+ t.equal(safeGlobal.bar, bar);
+
+ const foo = {};
+ t.throws(() => handler.set(target, 'foo', foo), /do not modify endowments like foo/);
+
+ t.equal(Object.keys(unsafeGlobal).length, 0);
+});

Scope handler inherits from throwinghandler