ses
Version:
Hardened JavaScript for Fearless Cooperation
90 lines (83 loc) • 3.55 kB
JavaScript
import { FERAL_EVAL, create, defineProperties, freeze } from './commons.js';
import { assert } from './error/assert.js';
const { Fail } = assert;
// We attempt to frustrate stack bumping attacks on the safe evaluator
// (`make-safe-evaluator.js`).
// A stack bumping attack forces an API call to throw a stack overflow
// `RangeError` at an inopportune time.
// The attacker arranges for the stack to be sufficiently deep that the API
// consumes exactly enough stack frames to throw an exception.
//
// For the safe evaluator, an exception thrown between adding and then deleting
// `eval` on `evalScope` could leak the real `eval` to an attacker's lexical
// scope.
// This would be sufficiently disastrous that we guard against it twice.
// First, we delete `eval` from `evalScope` immediately before rendering it to
// the guest program's lexical scope.
//
// If the attacker manages to arrange for `eval` to throw an exception after we
// call `allowNextEvalToBeUnsafe` but before the guest program accesses `eval`,
// it would be able to access `eval` once more in its own code.
// Although they could do no harm with a direct `eval`, they would be able to
// escape to the true global scope with an indirect `eval`.
//
// prepareStack(depth, () => {
// (eval)('');
// });
// const unsafeEval = (eval);
// const safeEval = (eval);
// const realGlobal = unsafeEval('globalThis');
//
// To protect against that case, we also delete `eval` from the `evalScope` in
// a `finally` block surrounding the call to the safe evaluator.
// The only way to reach this case is if `eval` remains on `evalScope` due to
// an attack, so we assume that attack would have have invalided our isolation
// and revoke all future access to the evaluator.
//
// To defeat a stack bumping attack, we must use fewer stack frames to recover
// in that `finally` block than we used in the `try` block.
// We have no reliable guarantees about how many stack frames a block of
// JavaScript will consume.
// Function inlining, tail-call optimization, variations in the size of a stack
// frame, and block scopes may affect the depth of the stack.
// The only number of acceptable stack frames to use in the finally block is
// zero.
// We only use property assignment and deletion in the safe evaluator's
// `finally` block.
// We use `delete evalScope.eval` to withhold the evaluator.
// We assign an envelope object over `evalScopeKit.revoked` to revoke the
// evaluator.
//
// This is why we supply a meaningfully named function for
// `allowNextEvalToBeUnsafe` but do not provide a corresponding
// `revokeAccessToUnsafeEval` or even simply `revoke`.
// These recovery routines are expressed inline in the safe evaluator.
export const makeEvalScopeKit = () => {
const evalScope = create(null);
const oneTimeEvalProperties = freeze({
eval: {
get() {
delete evalScope.eval;
return FERAL_EVAL;
},
enumerable: false,
configurable: true,
},
});
const evalScopeKit = {
evalScope,
allowNextEvalToBeUnsafe() {
const { revoked } = evalScopeKit;
if (revoked !== null) {
Fail`a handler did not reset allowNextEvalToBeUnsafe ${revoked.err}`;
}
// Allow next reference to eval produce the unsafe FERAL_EVAL.
// We avoid defineProperty because it consumes an extra stack frame taming
// its return value.
defineProperties(evalScope, oneTimeEvalProperties);
},
/** @type {null | { err: any }} */
revoked: null,
};
return evalScopeKit;
};