ses
Version:
Hardened JavaScript for Fearless Cooperation
111 lines (102 loc) • 4.72 kB
JavaScript
// @ts-check
import { FERAL_FUNCTION, arrayJoin, apply } from './commons.js';
import { getScopeConstants } from './scope-constants.js';
/**
* buildOptimizer()
* Given an array of identifiers, the optimizer returns a `const` declaration
* destructuring `this.${name}`.
*
* @param {Array<string>} constants
* @param {string} name
*/
function buildOptimizer(constants, name) {
// No need to build an optimizer when there are no constants.
if (constants.length === 0) return '';
// Use 'this' to avoid going through the scope proxy, which is unnecessary
// since the optimizer only needs references to the safe global.
// Destructure the constants from the target scope object.
return `const {${arrayJoin(constants, ',')}} = this.${name};`;
}
/**
* makeEvaluate()
* Create an 'evaluate' function with the correct optimizer inserted.
*
* @param {object} context
* @param {object} context.evalScope
* @param {object} context.moduleLexicals
* @param {object} context.globalObject
* @param {object} context.scopeTerminator
*/
export const makeEvaluate = context => {
const { globalObjectConstants, moduleLexicalConstants } = getScopeConstants(
context.globalObject,
context.moduleLexicals,
);
const globalObjectOptimizer = buildOptimizer(
globalObjectConstants,
'globalObject',
);
const moduleLexicalOptimizer = buildOptimizer(
moduleLexicalConstants,
'moduleLexicals',
);
// Create a function in sloppy mode, so that we can use 'with'. It returns
// a function in strict mode that evaluates the provided code using direct
// eval, and thus in strict mode in the same scope. We must be very careful
// to not create new names in this scope
// 1: we use multiple nested 'with' to catch all free variable names. The
// `this` value of the outer sloppy function holds the different scope
// layers, from inner to outer:
// a) `evalScope` which must release the `FERAL_EVAL` as 'eval' once for
// every invocation of the inner `evaluate` function in order to
// trigger direct eval. The direct eval semantics is what allows the
// evaluated code to lookup free variable names on the other scope
// objects and not in global scope.
// b) `moduleLexicals` which provide a way to introduce free variables
// that are not available on the globalObject.
// c) `globalObject` is the global scope object of the evaluator, aka the
// Compartment's `globalThis`.
// d) `scopeTerminator` is a proxy object which prevents free variable
// lookups to escape to the start compartment's global object.
// 2: `optimizer`s catch constant variable names for speed.
// 3: The inner strict `evaluate` function should be passed two parameters:
// a) its arguments[0] is the source to be directly evaluated.
// b) its 'this' is the this binding seen by the code being
// directly evaluated (the globalObject).
// Notes:
// - The `optimizer` strings only lookup values on the `globalObject` and
// `moduleLexicals` objects by construct. Keywords like 'function' are
// reserved and cannot be used as a variable, so they are excluded from the
// optimizer. Furthermore to prevent shadowing 'eval', while a valid
// identifier, that name is also explicitly excluded.
// - when 'eval' is looked up in the `evalScope`, the powerful unsafe eval
// intrinsic is returned after automatically removing it from the
// `evalScope`. Any further reference to 'eval' in the evaluate string will
// get the tamed evaluator from the `globalObject`, if any.
// TODO https://github.com/endojs/endo/issues/816
// The optimizer currently runs under sloppy mode, and although we doubt that
// there is any vulnerability introduced just by running the optimizer
// sloppy, we are much more confident in the semantics of strict mode.
// The `evaluate` function can be and is reused across multiple evaluations.
// Since the optimizer should not be re-evaluated every time, it cannot be
// inside the `evaluate` closure. While we could potentially nest an
// intermediate layer of `() => {'use strict'; ${optimizers}; ...`, it
// doesn't seem worth the overhead and complexity.
const evaluateFactory = FERAL_FUNCTION(`
with (this.scopeTerminator) {
with (this.globalObject) {
with (this.moduleLexicals) {
with (this.evalScope) {
${globalObjectOptimizer}
${moduleLexicalOptimizer}
return function() {
'use strict';
return eval(arguments[0]);
};
}
}
}
}
`);
return apply(evaluateFactory, context, []);
};