Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Workaround for override mistake #146

Merged
merged 11 commits into from
Aug 27, 2019
6 changes: 6 additions & 0 deletions src/bundle/createSES.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import getAllPrimordials from './getAllPrimordials';
import whitelist from './whitelist';
import makeConsole from './make-console';
import makeMakeRequire from './make-require';
import makeRepairDataProperties from './makeRepairDataProperties';

const FORWARDED_REALMS_OPTIONS = ['transforms'];

Expand Down Expand Up @@ -117,6 +118,9 @@ You probably want a Compartment instead, like:

const r = Realm.makeRootRealm({ ...realmsOptions, shims });

const makeRepairDataPropertiesSrc = `(${makeRepairDataProperties})`;
const repairDataProperties = r.evaluate(makeRepairDataPropertiesSrc)();

// Build a harden() with an empty fringe. It will be populated later when
// we call harden(allIntrinsics).
const makeHardenerSrc = `(${makeHardener})`;
Expand All @@ -138,6 +142,8 @@ You probably want a Compartment instead, like:
r.global,
anonIntrinsics,
);

repairDataProperties(allIntrinsics);
harden(allIntrinsics);

// build the makeRequire helper, glue it to the new Realm
Expand Down
147 changes: 147 additions & 0 deletions src/bundle/makeRepairDataProperties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Adapted from SES/Caja
// Copyright (C) 2011 Google Inc.
// https://github.com/google/caja/blob/master/src/com/google/caja/ses/startSES.js
// https://github.com/google/caja/blob/master/src/com/google/caja/ses/repairES5.js

export default function makeRepairDataProperties() {
const {
defineProperties,
getOwnPropertyDescriptors,
hasOwnProperty,
} = Object;
const { ownKeys } = Reflect;

// Object.defineProperty is allowed to fail silently,
// wrap Object.defineProperties instead.
function defineProperty(obj, prop, desc) {
defineProperties(obj, { [prop]: desc });
}

/**
* For a special set of properties (defined below), it ensures that the
* effect of freezing does not suppress the ability to override these
* properties on derived objects by simple assignment.
*
* Because of lack of sufficient foresight at the time, ES5 unfortunately
* specified that a simple assignment to a non-existent property must fail if
* it would override a non-writable data property of the same name. (In
* retrospect, this was a mistake, but it is now too late and we must live
* with the consequences.) As a result, simply freezing an object to make it
* tamper proof has the unfortunate side effect of breaking previously correct
* code that is considered to have followed JS best practices, if this
* previous code used assignment to override.
*
* To work around this mistake, deepFreeze(), prior to freezing, replaces
* selected configurable own data properties with accessor properties which
* simulate what we should have specified -- that assignments to derived
* objects succeed if otherwise possible.
*/
function enableDerivedOverride(obj, prop, desc) {
if ('value' in desc && desc.configurable) {
const { value } = desc;

// eslint-disable-next-line no-inner-declarations
function getter() {
return value;
}

// Re-attach the data property on the object so
// it can be found by the deep-freeze traversal process.
getter.value = value;

// eslint-disable-next-line no-inner-declarations
function setter(newValue) {
if (obj === this) {
throw new TypeError(
`Cannot assign to read only property '${prop}' of object '${obj}'`,
);
}
if (hasOwnProperty.call(this, prop)) {
this[prop] = newValue;
} else {
defineProperty(this, prop, {
value: newValue,
writable: true,
enumerable: desc.enumerable,
configurable: desc.configurable,
});
}
}

defineProperty(obj, prop, {
get: getter,
set: setter,
enumerable: desc.enumerable,
configurable: desc.configurable,
});
}
}

/**
* These properties are subject to the override mistake
* and must be converted before freezing.
*/
function repairDataProperties(intrinsics) {
const { global: g, anonIntrinsics: a } = intrinsics;

const toBeRepaired = [
g.Object.prototype,
g.Array.prototype,
// g.Boolean.prototype,
// g.Date.prototype,
// g.Number.prototype,
// g.String.prototype,
// g.RegExp.prototype,

g.Function.prototype,
a.GeneratorFunction.prototype,
a.AsyncFunction.prototype,
a.AsyncGeneratorFunction.prototype,

a.IteratorPrototype,
// a.ArrayIteratorPrototype,

// g.DataView.prototype,

a.TypedArray.prototype,
// g.Int8Array.prototype,
// g.Int16Array.prototype,
// g.Int32Array.prototype,
// g.Uint8Array.prototype,
// g.Uint16Array.prototype,
// g.Uint32Array.prototype,

g.Error.prototype,
// g.EvalError.prototype,
// g.RangeError.prototype,
// g.ReferenceError.prototype,
// g.SyntaxError.prototype,
// g.TypeError.prototype,
// g.URIError.prototype,
];

// Promise may be removed from the whitelist
// TODO: the toBeRepaired list should be prepared
// externally and provided to repairDataProperties
const PromisePrototype = g.Promise && g.Promise.prototype;
if (PromisePrototype) {
toBeRepaired.push(PromisePrototype);
}

// repair each entry
toBeRepaired.forEach(obj => {
if (!obj) {
return;
}
const descs = getOwnPropertyDescriptors(obj);
if (!descs) {
return;
}
ownKeys(obj).forEach(prop =>
enableDerivedOverride(obj, prop, descs[prop]),
);
});
}

return repairDataProperties;
}
53 changes: 53 additions & 0 deletions test/test-repairDataProperties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import test from 'tape';
import SES from '../src/index';

test('Can assign "toString" of constructor prototype', t => {
const s = SES.makeSESRootRealm();
function testContent() {
function Animal() {}
Animal.prototype.toString = () => 'moo';
const animal = new Animal();
return animal.toString();
}
try {
const result = s.evaluate(`(${testContent})`)();
t.equal(result, 'moo');
} catch (err) {
t.fail(err);
}
t.end();
});

test('Can assign "toString" of class prototype', t => {
const s = SES.makeSESRootRealm();
function testContent() {
class Animal {}
Animal.prototype.toString = () => 'moo';
const animal = new Animal();
return animal.toString();
}
try {
const result = s.evaluate(`(${testContent})`)();
t.equal(result, 'moo');
} catch (err) {
t.fail(err);
}
t.end();
});

test('Can assign "slice" of Array-inherited class prototype', t => {
const s = SES.makeSESRootRealm();
function testContent() {
class Pizza extends Array {}
Pizza.prototype.slice = () => ['yum'];
const pizza = new Pizza();
return pizza.slice();
}
try {
const result = s.evaluate(`(${testContent})`)();
t.deepEqual(result, ['yum']);
} catch (err) {
t.fail(err);
}
t.end();
});