fix(ses): Refactor Compartment to use shared evaluator
Fixes `eval` and `Function` constructor not having global lexical scope or using known scope proxies
MODIFY
packages/ses/src/compartment-evaluate.js
packages/ses/src/compartment-evaluate.js
@@ -41,8 +41,20 @@ export const compartmentEvaluate = (compartmentFields, source, options) => {
arrayPush(localTransforms, rejectSomeDirectEvalExpressions);
}
+ let evaluate;
+
+ if (__moduleShimLexicals__ === undefined && !sloppyGlobalsMode) {
+ ({ evaluate } = compartmentFields);
+ } else {
+ // The scope proxy or global lexicals are different from the
+ // shared evaluator so we need to build a new one
+
let { globalTransforms } = compartmentFields;
- const { globalObject, globalLexicals, knownScopeProxies } = compartmentFields;
+ const {
+ globalObject,
+ globalLexicals,
+ knownScopeProxies,
+ } = compartmentFields;
let localObject = globalLexicals;
if (__moduleShimLexicals__ !== undefined) {
@@ -63,14 +75,14 @@ export const compartmentEvaluate = (compartmentFields, source, options) => {
);
}
- const evaluate = makeEvaluate({
+ evaluate = makeEvaluate({
globalObject,
localObject,
globalTransforms,
sloppyGlobalsMode,
knownScopeProxies,
});
-
+ }
return evaluate(source, {
localTransforms,
});

MODIFY
packages/ses/src/compartment-shim.js
packages/ses/src/compartment-shim.js
@@ -31,6 +31,7 @@ import { link } from './module-link.js';
import { getDeferredExports } from './module-proxy.js';
import { assert } from './error/assert.js';
import { compartmentEvaluate } from './compartment-evaluate.js';
+import { makeEvaluate } from './evaluate.js';
const { quote: q } = assert;
@@ -269,21 +270,27 @@ export const makeCompartmentConstructor = (
// when transferred-by-property-descriptor onto local scope objects.
const globalLexicals = freeze({ ...globalLexicalsOption });
- const knownScopeProxies = new WeakSet();
-
const globalObject = {};
initGlobalObjectConstants(globalObject);
+
+ const knownScopeProxies = new WeakSet();
+ const evaluate = makeEvaluate({
+ globalObject,
+ localObject: globalLexicals,
+ globalTransforms,
+ sloppyGlobalsMode: false,
+ knownScopeProxies,
+ });
+
initGlobalObjectProperties(
globalObject,
intrinsics,
sharedGlobalPropertyNames,
targetMakeCompartmentConstructor,
this.constructor.prototype,
- {
- globalTransforms,
+ evaluate,
markVirtualizedNativeFunction,
- },
);
assign(globalObject, endowments);
@@ -294,6 +301,7 @@ export const makeCompartmentConstructor = (
globalObject,
knownScopeProxies,
globalLexicals,
+ evaluate,
resolveHook,
importHook,
moduleMap,

MODIFY
packages/ses/src/global-object.js
packages/ses/src/global-object.js
@@ -35,9 +35,8 @@ export const initGlobalObjectConstants = globalObject => {
* @param {Object} newGlobalPropertyNames
* @param {Function} makeCompartmentConstructor
* @param {Object} compartmentPrototype
- * @param {Object} [options]
- * @param {Array<Transform>} [options.globalTransforms]
- * @param {(Object) => void} [options.markVirtualizedNativeFunction]
+ * @param {(string, Object?) => any} evaluate
+ * @param {(Object) => void} markVirtualizedNativeFunction
*/
export const initGlobalObjectProperties = (
globalObject,
@@ -45,7 +44,8 @@ export const initGlobalObjectProperties = (
newGlobalPropertyNames,
makeCompartmentConstructor,
compartmentPrototype,
- { globalTransforms, markVirtualizedNativeFunction },
+ evaluate,
+ markVirtualizedNativeFunction,
) => {
for (const [name, intrinsicName] of entries(universalPropertyNames)) {
if (objectHasOwnProperty(intrinsics, intrinsicName)) {
@@ -71,12 +71,8 @@ export const initGlobalObjectProperties = (
const perCompartmentGlobals = {
globalThis: globalObject,
- eval: makeEvalFunction(globalObject, {
- globalTransforms,
- }),
- Function: makeFunctionConstructor(globalObject, {
- globalTransforms,
- }),
+ eval: makeEvalFunction(evaluate),
+ Function: makeFunctionConstructor(evaluate),
};
perCompartmentGlobals.Compartment = makeCompartmentConstructor(

MODIFY
packages/ses/src/lockdown-shim.js
packages/ses/src/lockdown-shim.js
@@ -28,6 +28,7 @@ import {
initGlobalObjectConstants,
initGlobalObjectProperties,
} from './global-object.js';
+import { makeEvaluate } from './evaluate.js';
import { initialGlobalPropertyNames } from './whitelist.js';
import { tameFunctionToString } from './tame-function-tostring.js';
import { tameDomains } from './tame-domains.js';
@@ -293,15 +294,17 @@ export const repairIntrinsics = (
// Initialize the powerful initial global, i.e., the global of the
// start compartment, from the intrinsics.
initGlobalObjectConstants(globalThis);
+
+ const evaluate = makeEvaluate({ globalObject: globalThis });
+
initGlobalObjectProperties(
globalThis,
intrinsics,
initialGlobalPropertyNames,
makeCompartmentConstructor,
compartmentPrototype,
- {
+ evaluate,
markVirtualizedNativeFunction,
- },
);
/**

MODIFY
packages/ses/src/make-eval-function.js
packages/ses/src/make-eval-function.js
-import { makeEvaluate } from './evaluate.js';
-
/*
* makeEvalFunction()
* A safe version of the native eval function which relies on
* the safety of makeEvaluate for confinement.
*/
-export const makeEvalFunction = (globalObject, options = {}) => {
+export const makeEvalFunction = evaluate => {
// We use the the concise method syntax to create an eval without a
// [[Construct]] behavior (such that the invocation "new eval()" throws
// TypeError: eval is not a constructor"), but which still accepts a
@@ -19,7 +17,6 @@ export const makeEvalFunction = (globalObject, options = {}) => {
// rule. Track.
return source;
}
- const evaluate = makeEvaluate({ ...options, globalObject });
return evaluate(source);
},
}.eval;

MODIFY
packages/ses/src/make-function-constructor.js
packages/ses/src/make-function-constructor.js
@@ -5,7 +5,6 @@ import {
defineProperties,
getPrototypeOf,
} from './commons.js';
-import { makeEvaluate } from './evaluate.js';
import { assert } from './error/assert.js';
/*
@@ -13,7 +12,7 @@ import { assert } from './error/assert.js';
* A safe version of the native Function which relies on
* the safety of makeEvaluate for confinement.
*/
-export const makeFunctionConstructor = (globalObject, options = {}) => {
+export const makeFunctionConstructor = evaluate => {
// Define an unused parameter to ensure Function.length === 1
const newFunction = function Function(_body) {
// Sanitize all parameters at the entry point.
@@ -53,7 +52,6 @@ export const makeFunctionConstructor = (globalObject, options = {}) => {
// TODO: since we create an anonymous function, the 'this' value
// isn't bound to the global object as per specs, but set as undefined.
const src = `(function anonymous(${parameters}\n) {\n${bodyText}\n})`;
- const evaluate = makeEvaluate({ ...options, globalObject });
return evaluate(src);
};

MODIFY
packages/ses/test/test-compartment-known-scope-proxy.js
packages/ses/test/test-compartment-known-scope-proxy.js
@@ -11,13 +11,27 @@ function leakScopeProxy() {
test('SES compartment recognizes its own scopeProxies', t => {
const c = new Compartment({ leakScopeProxy });
const scopeProxy1 = c.evaluate('leakScopeProxy()');
- const scopeProxy2 = c.evaluate('leakScopeProxy()');
+ const scopeProxy2 = c.evaluate('leakScopeProxy()', {
+ // Force a new evaluate to be created
+ __moduleShimLexicals__: {},
+ });
+ const scopeProxyEval = c.globalThis.eval('leakScopeProxy()');
+ const scopeProxyFunction = new c.globalThis.Function(
+ 'return leakScopeProxy()',
+ )();
t.is(1, 1);
t.not(scopeProxy1, scopeProxy2);
+ // c.evaluate, globalThis.Function and globalThis.eval share evaluator
+ t.is(scopeProxy1, scopeProxyEval);
+ t.is(scopeProxy1, scopeProxyFunction);
t.is(typeof scopeProxy1, 'object');
t.is(typeof scopeProxy2, 'object');
+ t.is(typeof scopeProxyEval, 'object');
+ t.is(typeof scopeProxyFunction, 'object');
t.is(c.__isKnownScopeProxy__(scopeProxy1), true);
t.is(c.__isKnownScopeProxy__(scopeProxy2), true);
+ t.is(c.__isKnownScopeProxy__(scopeProxyEval), true);
+ t.is(c.__isKnownScopeProxy__(scopeProxyFunction), true);
t.is(c.__isKnownScopeProxy__({}), false);
});

MODIFY
packages/ses/test/test-global-lexicals-evaluate.js
packages/ses/test/test-global-lexicals-evaluate.js
@@ -213,3 +213,28 @@ test('global lexical accessors receive globalThis', t => {
// it to be captured.
t.is(receiver, globalLexicals);
});
+
+test('global lexicals are set in all evaluators', t => {
+ t.plan(4);
+
+ const endowments = {};
+ const modules = {};
+ const globalLexicals = { hello: 'World!' };
+ const compartment = new Compartment(endowments, modules, { globalLexicals });
+
+ const whom = compartment.evaluate('hello');
+
+ t.is(
+ compartment.globalThis.eval('typeof hello'),
+ 'string',
+ 'Compartment `eval` has global lexical scope',
+ );
+ t.is(
+ new compartment.globalThis.Function('return typeof hello')(),
+ 'string',
+ 'Compartment `Function` constructor has global lexical scope',
+ );
+
+ t.is(compartment.globalThis.eval('hello'), whom);
+ t.is(new compartment.globalThis.Function('return hello')(), whom);
+});

MODIFY
packages/ses/test/test-global-lexicals-modules.js
packages/ses/test/test-global-lexicals-modules.js
@@ -17,6 +17,11 @@ const makeCompartment = (endowments, globalLexicals) => {
import { whom } from './main.js';
export default whom;
`,
+ 'https://example.com/packages/example/evaluators.js': `
+ export const whom = hello;
+ export const whomEval = (0,eval)("typeof hello === 'string' ? hello : undefined");
+ export const whomFunction = new Function("return typeof hello === 'string' ? hello : undefined")();
+ `,
});
const compartment = new Compartment(
endowments,
@@ -222,3 +227,16 @@ test('global lexical overshadowed by imported name', async t => {
const { namespace } = await compartment.import('./collision.js');
t.is(namespace.default, 'World!');
});
+
+test('endowments own properties are set in all evaluators', async t => {
+ t.plan(3);
+
+ const endowments = { hello: 'World!' };
+ const compartment = makeCompartment(endowments);
+
+ const { namespace } = await compartment.import('./evaluators.js');
+ const { whom, whomEval, whomFunction } = namespace;
+ t.is(whom, 'World!');
+ t.is(whomEval, whom);
+ t.is(whomFunction, whom);
+});

MODIFY
packages/ses/test/test-global-object.js
packages/ses/test/test-global-object.js
@@ -24,15 +24,15 @@ test('globalObject', t => {
const globalObject = {};
initGlobalObjectConstants(globalObject);
+ const evaluate = (_source, _options) => {};
initGlobalObjectProperties(
globalObject,
intrinsics,
sharedGlobalPropertyNames,
makeCompartmentConstructor,
CompartmentPrototype,
- {
- markVirtualizedNativeFunction(_func) {},
- },
+ evaluate,
+ _ => {},
);
t.truthy(globalObject instanceof Object);

MODIFY
packages/ses/test/test-make-eval-function.js
packages/ses/test/test-make-eval-function.js
@@ -4,12 +4,14 @@ import '../index.js';
import './lockdown-safe.js';
import test from 'ava';
import { makeEvalFunction } from '../src/make-eval-function.js';
+import { makeEvaluate } from '../src/evaluate.js';
test('makeEvalFunction - leak', t => {
t.plan(8);
const globalObject = {};
- const safeEval = makeEvalFunction(globalObject);
+ const evaluate = makeEvaluate({ globalObject });
+ const safeEval = makeEvalFunction(evaluate);
t.throws(() => safeEval('none'), { instanceOf: ReferenceError });
t.is(safeEval('this.none'), undefined);
@@ -40,7 +42,8 @@ test('makeEvalFunction - globals', t => {
bar: { value: 2, writable: true },
},
);
- const safeEval = makeEvalFunction(globalObject);
+ const evaluate = makeEvaluate({ globalObject });
+ const safeEval = makeEvalFunction(evaluate);
t.is(safeEval('foo'), 1);
t.is(safeEval('bar'), 2);

MODIFY
packages/ses/test/test-make-function-constructor.js
packages/ses/test/test-make-function-constructor.js
@@ -2,6 +2,7 @@ import '../index.js';
import './lockdown-safe.js';
import test from 'ava';
import { makeFunctionConstructor } from '../src/make-function-constructor.js';
+import { makeEvaluate } from '../src/evaluate.js';
test('functionConstructor', t => {
t.plan(12);
@@ -13,7 +14,8 @@ test('functionConstructor', t => {
bar: { value: 2, writable: true },
},
);
- const safeFunction = makeFunctionConstructor(globalObject);
+ const evaluate = makeEvaluate({ globalObject });
+ const safeFunction = makeFunctionConstructor(evaluate);
t.is(safeFunction('return foo')(), 1);
t.is(safeFunction('return bar')(), 2);

fix(ses): Refactor Compartment to use shared evaluator
Fixes `eval` and `Function` constructor not having global lexical scope or using known scope proxies