-
Notifications
You must be signed in to change notification settings - Fork 19
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
Changes from 6 commits
18fcbaa
aa68ac8
014fda1
3108ad8
6630e69
25d1369
b8687f9
2fa711d
31ea209
39a7372
c962e27
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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, | ||
// 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
// 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; | ||
} |
There was a problem hiding this comment.
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 REPLThere was a problem hiding this comment.
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 anLet's say that this call reports success, however
defineProperty
is supposed to report success. Then, presumably, hw1 actually has a non-writable, non-configurablefoo
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-configurablefoo
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 nofoo
global variable. What shouldreturn? 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, werewp
to do so, after the previousdefineProperty
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 thatdefineProperty
call throw an error, which was the only way thatdefineProperty
could report that the operation failed. This broke some old code in some old version of some library that did not actually care whether thedefineProperty
succeeded, failed, or did anything at all. But if the call todefineProperty
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 ofdefineProperty
, that could only report failure by throwing.There was a problem hiding this comment.
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?There was a problem hiding this comment.
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 toObject.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?There was a problem hiding this comment.
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 - ifObject.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?There was a problem hiding this comment.
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.