Skip to content
Merged
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();
});