feat(ses): Add __shimTransforms__ Compartment option
MODIFY
packages/ses/NEWS.md
packages/ses/NEWS.md
@@ -7,6 +7,10 @@ User-visible changes in SES:
`assert` system generated other diagnostic information hidden in side
tables. The tamed console uses these side tables to output more informative
diagnostics. [Logging Errors](./src/error/README.md) explains the design.
+* Adds a non-standardizable `__shimTransforms__` option to the
+ Compartment constructor that allows a single transform to work
+ for both programs passed to `evaluate` and modules that the SES shim
+ compiles to programs.
## Release 0.10.4 (28-September-2020)

MODIFY
packages/ses/README.md
packages/ses/README.md
@@ -272,6 +272,69 @@ The compartment will call `execute` with:
method of third-party static module records to return promises, to support
top-level await.
+### Transforms
+
+The `Compartment` constructor accepts a `transforms` option.
+This is an array of JavaScript source to source translation functions,
+in the order they should be applied.
+Passing the source to the first function's input, then from each function's
+output to the next's input, the final function's output must be a valid
+JavaScript "Program" grammar construction, code that is valid in a `<script>`,
+not a module.
+
+```js
+const transforms = [addCodeCoverageInstrumentation];
+const globalLexicals = { coverage };
+const c = new Compartment({ console }, null, { transforms, globalLexicals });
+c.evaluate('console.log("Hello");');
+```
+
+The `evaluate` method of a compartment also accepts a `transforms` option.
+These apply before and in addition to the compartment-scoped transforms.
+
+```js
+const transform = source => source.replace(/Farewell/g, 'Hello');
+const transforms = [transform];
+c.evaluate('console.log("Farewell, World!")', { transforms });
+// Hello, World!
+```
+
+These transforms do not apply to modules.
+To transform the source of an ECMAScript module, the `importHook` must
+intercept the source and transform it before passing it to the
+`StaticModuleRecord` constructor.
+These are distinct because programs and modules have distinct grammar
+productions.
+
+An **internal implementation detail** of the SES-shim is that it
+converts modules to programs and evaluates them as programs.
+So, only for this implementation of `Compartment`, it is possible for a program
+transform to be equally applicable for modules, but that transform will
+have a window into the internal translation, will be sensitive to changes to
+that translation between any pair of releases, even those that do not disclose
+any breaking changes, and will only work on SES-shim, not any other
+implementation of `Compartment` like the one provided by XS.
+
+The SES-shim `Compartment` constructor accepts a `__shimTransforms__`
+option for this purpose.
+For the `Compartment` to use the same transforms for both evaluated strings
+and modules converted to programs, pass them as `__shimTransforms__`
+instead of `transforms`.
+
+```js
+const __shimTransforms__ = [addMetering];
+const globalLexicals = { meter };
+const c = new Compartment({ console }, null, {
+ __shimTransforms__,
+ globalLexicals
+});
+c.evaluate('console.log("Hello");');
+```
+
+The `__shimTransforms__` feature is designed to uphold the security properties
+of compartments, since an attacker may use all available features, whether they
+are standard or not.
+
### Logging Errors
`lockdown()` adds new global `assert` and tames the global `console`. The error

MODIFY
packages/ses/src/compartment-shim.js
packages/ses/src/compartment-shim.js
@@ -51,14 +51,22 @@ export const CompartmentPrototype = {
} = options;
const localTransforms = [...transforms];
- const {
- globalTransforms,
- globalObject,
- globalLexicals,
- } = privateFields.get(this);
+ const compartmentFields = privateFields.get(this);
+ let { globalTransforms } = compartmentFields;
+ const { globalObject, globalLexicals } = compartmentFields;
let localObject = globalLexicals;
if (__moduleShimLexicals__ !== undefined) {
+ // When using `evaluate` for ESM modules, as should only occur from the
+ // module-shim's module-instance.js, we do not reveal the SES-shim's
+ // module-to-program translation, as this is not standardizable behavior.
+ // However, the `localTransforms` will come from the `__shimTransforms__`
+ // Compartment option in this case, which is a non-standardizable escape
+ // hatch so programs designed specifically for the SES-shim
+ // implementation may opt-in to use the same transforms for `evaluate`
+ // and `import`, at the expense of being tightly coupled to SES-shim.
+ globalTransforms = undefined;
+
localObject = create(null, getOwnPropertyDescriptors(globalLexicals));
defineProperties(
localObject,
@@ -103,6 +111,7 @@ export const makeCompartmentConstructor = (
const {
name = '<unknown>',
transforms = [],
+ __shimTransforms__ = [],
globalLexicals = {},
} = options;
const globalTransforms = [...transforms];

MODIFY
packages/ses/src/module-instance.js
packages/ses/src/module-instance.js
@@ -29,6 +29,8 @@ export const makeModuleInstance = (
const compartmentFields = privateFields.get(compartment);
+ const { __shimTransforms__ } = compartmentFields;
+
const { exportsProxy, proxiedExports, activate } = getDeferredExports(
compartment,
compartmentFields,
@@ -318,6 +320,7 @@ export const makeModuleInstance = (
let optFunctor = compartment.evaluate(functorSource, {
globalObject,
+ transforms: __shimTransforms__,
__moduleShimLexicals__: localLexicals,
});
let didThrow = false;

MODIFY
packages/ses/src/module-shim.js
packages/ses/src/module-shim.js
@@ -198,7 +198,12 @@ export const makeModularCompartmentConstructor = (
new.target,
);
- const { resolveHook, importHook, moduleMapHook } = options;
+ const {
+ resolveHook,
+ importHook,
+ moduleMapHook,
+ __shimTransforms__,
+ } = options;
// Map<FullSpecifier, ModuleCompartmentRecord>
const moduleRecords = new Map();
@@ -237,6 +242,7 @@ export const makeModularCompartmentConstructor = (
moduleMap,
moduleMapHook,
moduleRecords,
+ __shimTransforms__,
deferredExports,
instances,
});

ADD
packages/ses/test/compartment-transforms.test.js
+import tap from 'tap';
+import '../ses.js';
+
+const { test } = tap;
+
+lockdown();
+
+test('transforms apply to evaluated expressions', t => {
+ t.plan(1);
+
+ const transform = source => source.replace(/Farewell/g, 'Hello');
+ const transforms = [transform];
+ const c = new Compartment({}, {}, { transforms });
+ const greeting = c.evaluate('"Farewell, World!"');
+
+ t.equal(greeting, 'Hello, World!');
+});
+
+test('transforms apply to dynamic eval in compartments', t => {
+ t.plan(1);
+
+ const transform = source => source.replace(/Farewell/g, 'Hello');
+ const transforms = [transform];
+ const c = new Compartment(
+ {
+ greeting: '"Farewell, World!"',
+ },
+ {},
+ { transforms },
+ );
+ const greeting = c.evaluate('(0, eval)(greeting)');
+
+ t.equal(greeting, 'Hello, World!');
+});
+
+test('transforms do not apply to dynamic eval in compartments within compartments', t => {
+ t.plan(1);
+
+ const transform = source => source.replace(/Farewell/g, 'Hello');
+ const transforms = [transform];
+ const c = new Compartment({}, {}, { transforms });
+ const d = c.evaluate('new Compartment()');
+ const greeting = d.evaluate('"Farewell, World!"');
+
+ t.equal(greeting, 'Farewell, World!');
+});
+
+test('transforms do not apply to imported modules', async t => {
+ t.plan(1);
+
+ const transform = source => source.replace(/Farewell/g, 'Hello');
+ const transforms = [transform];
+ const resolveHook = () => '';
+ const importHook = () =>
+ new StaticModuleRecord('export default "Farewell, World!";');
+ const c = new Compartment({}, {}, { transforms, resolveHook, importHook });
+
+ const { namespace } = await c.import('any-string-here');
+ const { default: greeting } = namespace;
+
+ t.equal(greeting, 'Farewell, World!');
+});
+
+test('__shimTransforms__ apply to evaluated expressions', t => {
+ t.plan(1);
+
+ const transform = source => source.replace(/Farewell/g, 'Hello');
+ const transforms = [transform];
+ const c = new Compartment({}, {}, { transforms });
+ const greeting = c.evaluate('"Farewell, World!"');
+
+ t.equal(greeting, 'Hello, World!');
+});
+
+test('__shimTransforms__ do apply to imported modules', async t => {
+ t.plan(1);
+
+ const transform = source => source.replace(/Farewell/g, 'Hello');
+ const transforms = [transform];
+ const resolveHook = () => '';
+ const importHook = () =>
+ new StaticModuleRecord('export default "Farewell, World!";');
+ const c = new Compartment(
+ {},
+ {},
+ { __shimTransforms__: transforms, resolveHook, importHook },
+ );
+
+ const { namespace } = await c.import('any-string-here');
+ const { default: greeting } = namespace;
+
+ t.equal(greeting, 'Hello, World!');
+});

feat(ses): Add __shimTransforms__ Compartment option (#485)