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 './mutable';

const FORWARDED_REALMS_OPTIONS = ['sloppyGlobals', 'transforms'];

Expand Down Expand Up @@ -109,6 +110,9 @@ export function createSESWithRealmConstructor(creatorStrings, Realm) {

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 @@ -130,6 +134,8 @@ export function createSESWithRealmConstructor(creatorStrings, Realm) {
r.global,
anonIntrinsics,
);

repairDataProperties(allIntrinsics);
harden(allIntrinsics);

// build the makeRequire helper, glue it to the new Realm
Expand Down
144 changes: 144 additions & 0 deletions src/bundle/mutable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// 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,
Copy link
Contributor Author

@kumavis kumavis Aug 26, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was based off a comment I found in realms-shim/src/common, but doesn't seem to be true in at least node v10 REPL

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another obscure corner of JavaScript. The only case where this might happen is on browsers, where there's a distinction between the reified so-called "WindowProxy" object and the unreified so-called "Window" object. The issue is that a browser frame, when it is navigated from url1 to url2, gets the completely new root realm associated with url2, including global variable bindings, but keeps the same global "window" object (which is actually a WindowProxy) because that is the same object the parent can hold onto to designate this frame no matter what it is navigated to.

The way this is implemented is that every root realm that occupied the frame has its own hidden so-called "Window" object, whose properties are aliased to that root realm's global variables. The so-called "WindowProxy" object has a current hidden "Window" object, that it forwards all operations to. Done naively, this leads to a contradiction:

Say wp is such a WindowProxy object. While it is at url1, and therefore forwarding to hidden window hw1, someone does an

Object.defineProperty(wp, 'foo', {
  value: 88,
  writable: false,
  configurable: false
});

Let's say that this call reports success, however defineProperty is supposed to report success. Then, presumably, hw1 actually has a non-writable, non-configurable foo data property with value 88. By the object invariants, these conditions represent a commitment that this property on this object must always be a non-writable, non-configurable foo data property with value 88. Fine so far.

Now some navigates the frame to url2, associated with hidden window hw2 that has no foo property, and whose root realm therefore also has no foo global variable. What should

Object.getOwnPropertyDescriptor(wp, 'foo')

return? What it actually does across all browsers, and as mandated by the html spec, is to forward that getOwnPropertyDescriptor request to its current hidden window object, hw2, which returns undefined. However, were wp to do so, after the previous defineProperty had indicated success, it would violate the previous commitment.

Therefore, the previous call to defineProperty must not report success. Heroic work by Mozilla on Firefox at first had that defineProperty call throw an error, which was the only way that defineProperty could report that the operation failed. This broke some old code in some old version of some library that did not actually care whether the defineProperty succeeded, failed, or did anything at all. But if the call to defineProperty threw, the library broke.

Therefore, for this one horrible special case, we allow defineProperty to report failure by returning false rather than throwing. SES does not care about breaking that old version of some obscure library. SES does care about the threat to integrity if code proceeds normally under the assumption that some operation succeeded when it didn't. Thus, SES should restore the less kludgy behavior of defineProperty, that could only report failure by throwing.

Copy link
Contributor Author

@kumavis kumavis Aug 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now should I continue to use Object.defineProperties as workaround? or something else?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attn @ljharb Do you know the status of the possibility that the failure masking behavior of Object.defineProperty be extended to Object.defineProperties? In your opinion, should we switch to another technique to unmask the failure masking, to stay safe in the face of this possible change?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on what I'm reading in this thread, the only reason Object.defineProperty was forced to not throw was for legacy web compat - if Object.defineProperties doesn't already have that problem, then I can't see why we'd ever want to make it not throw when it fails?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good. I agree with the conclusion. I was asking because I recall some tc39 talk of extending the failure masking behavior to Object.defineProperties for consistency. But I haven't heard much. If you haven't run across this, it is likely inactive. Whew. Thanks.

// 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 beMutable(obj, prop, desc) {
kumavis marked this conversation as resolved.
Show resolved Hide resolved
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, this was one of the things that Salesforce changed, though I do not remember to what.

Copy link
Contributor Author

@kumavis kumavis Aug 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its present in forcedotcom/aura, don't have access to their latest work for comparison.

also present in node's version

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getter.value = value; is fine.


// 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,
});
}
}

function beMutableProperties(obj) {
if (!obj) {
return;
}
const descs = getOwnPropertyDescriptors(obj);
if (!descs) {
return;
}
ownKeys(obj).forEach(prop => beMutable(obj, prop, descs[prop]));
}

/**
* 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,
kumavis marked this conversation as resolved.
Show resolved Hide resolved

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

a.IteratorPrototype,
a.ArrayIteratorPrototype,

g.DataView.prototype,

a.TypedArray,
a.TypedArray.prototype,
kumavis marked this conversation as resolved.
Show resolved Hide resolved
g.Int8Array.prototype,
g.Int16Array.prototype,
g.Int32Array.prototype,
g.Uint8Array,
g.Uint16Array,
g.Uint32Array,
kumavis marked this conversation as resolved.
Show resolved Hide resolved

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
const PromisePrototype = g.Promise && g.Promise.prototype;
kumavis marked this conversation as resolved.
Show resolved Hide resolved
if (PromisePrototype) {
toBeRepaired.push(PromisePrototype);
}

toBeRepaired.forEach(beMutableProperties);
}

return repairDataProperties;
}