UNPKG

ses

Version:

Hardened JavaScript for Fearless Cooperation

90 lines (83 loc) 3.55 kB
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; };