Prevent a frozen eval global property to break an invariant on the scope proxy
MODIFY
shim/src/evaluators.js
shim/src/evaluators.js
// https://github.com/v8/v8/blob/master/src/builtins/builtins-function.cc
import {
+ apply,
+ arrayJoin,
+ arrayPop,
+ arrayPush,
+ create,
defineProperty,
- setPrototypeOf,
- getPrototypeOf,
- getOwnPropertyNames,
getOwnPropertyDescriptors,
- arrayPush,
- arrayPop,
- arrayJoin,
- apply,
+ getOwnPropertyNames,
+ getPrototypeOf,
regexpMatch,
+ setPrototypeOf,
stringIncludes
} from './commons';
import { ScopeHandler } from './scopeHandler';
@@ -107,10 +108,14 @@ export function createSafeEvaluator(unsafeRec, safeGlobal) {
// This proxy has several functions:
// 1. works with the sentinel to alternate between direct eval and confined eval.
- // 2. shadows all properties of the hidden global by declaring them as undefined.
- // 3. resolves all existing properties of the sandboxed global.
+ // 2. shadows all properties of the unsafe global by declaring them as undefined.
+ // 3. resolves all existing properties of the safe global.
+ // 4. uses an empty object as the target, with the safe global as its prototype,
+ // to bypass a proxy invariant that would prevent alternating between different
+ // values of eval if the user was to freeze the eval property on the safe global.
const scopeHandler = new ScopeHandler(unsafeRec);
- const scopeProxy = new Proxy(safeGlobal, scopeHandler);
+ const scopeTarget = create(safeGlobal);
+ const scopeProxy = new Proxy(scopeTarget, scopeHandler);
const optimizableGlobals = getOptimizableGlobals(safeGlobal);
const scopedEvaluatorFactory = createScopedEvaluatorFactory(unsafeRec, optimizableGlobals);
@@ -188,7 +193,8 @@ export function createFunctionEvaluator(unsafeRec, safeEval) {
// function body. We coerce the body into a real string above to prevent
// someone from passing an object with a toString() that returns a safe
// string the first time, but an evil string the second time.
- new unsafeFunction(functionBody); // eslint-disable-line
+ // eslint-disable-next-line no-new, new-cap
+ new unsafeFunction(functionBody);
if (apply(stringIncludes, functionParams, [')'])) {
// If the formal parameters string include ) - an illegal

MODIFY
shim/src/scopeHandler.js
shim/src/scopeHandler.js
@@ -31,7 +31,7 @@ export class ScopeHandler {
}
if (prop === Symbol.unscopables) {
- // safe to return a primal realm Object here because the only code that
+ // Safe to return a primal realm Object here because the only code that
// can do a get() on a non-string is the internals of with() itself,
// and the only thing it does is to look for properties on it. User
// code cannot do a lookup on non-strings.
@@ -46,6 +46,16 @@ export class ScopeHandler {
return undefined;
}
+ // eslint-disable-next-line class-methods-use-this
+ set(target, prop, value) {
+ // Set the value on the shadow. The target itself is an empty
+ // object that is only used to prevent a froxen eval property.
+ // eslint-disable-next-line no-proto
+ target.__proto__[prop] = value;
+ // Return true after successful set.
+ return true;
+ }
+
// we need has() to return false for some names to prevent the lookup from
// climbing the scope chain and eventually reaching the unsafeGlobal
// object, which is bad.

MODIFY
shim/src/utilities.js
shim/src/utilities.js
@@ -7,10 +7,13 @@ export function throwTantrum(s, err = undefined) {
const msg = `please report internal shim error: ${s}`;
// note: we really do want to log these 'should never happen' things. there
// might be a better way to convince the linter, though.
- console.log(msg); // eslint-disable-line
+ // eslint-disable-next-line no-console
+ console.log(msg);
if (err) {
- console.log(`${err}`); // eslint-disable-line
- console.log(`${err.stack}`); // eslint-disable-line
+ // eslint-disable-next-line no-console
+ console.log(`${err}`);
+ // eslint-disable-next-line no-console
+ console.log(`${err.stack}`);
}
throw msg;
}

MODIFY
shim/test/realm/function-eval.js
shim/test/realm/function-eval.js
@@ -66,3 +66,16 @@ test('function-legitimate-but-weird-parameters', t => {
t.end();
});
+
+test('frozen-eval', t => {
+ const r = Realm.makeRootRealm();
+
+ const desc = Object.getOwnPropertyDescriptor(r.global, 'eval');
+ desc.writable = false;
+ desc.configurable = false;
+ Object.defineProperty(r.global, 'eval', desc);
+
+ t.equal(r.evaluate('eval(1)'), 1);
+
+ t.end();
+});

Prevent a frozen eval global property to break an invariant on the scope proxy