ses
Version:
Hardened JavaScript for Fearless Cooperation
212 lines (195 loc) • 7.06 kB
JavaScript
// Adapted from SES/Caja
// Copyright (C) 2011 Google Inc.
// @ts-check
import {
Set,
String,
TypeError,
arrayForEach,
defineProperty,
getOwnPropertyDescriptor,
getOwnPropertyDescriptors,
isObject,
objectHasOwnProperty,
ownKeys,
setHas,
} from './commons.js';
import {
minEnablements,
moderateEnablements,
severeEnablements,
} from './enablements.js';
/** @import {Reporter} from './reporting-types.js' */
/**
* For a special set of properties defined in the `enablement` list,
* `enablePropertyOverrides` 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 an non-writable data property of the same name in the
* shadow of the prototype chain. In retrospect, this was a mistake, the
* so-called "override 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.
*
* For the enabled properties, `enablePropertyOverrides` effectively shims what
* the assignment behavior would have been in the absence of the override
* mistake. However, the shim produces an imperfect emulation. It shims the
* behavior by turning these data properties into accessor properties, where
* the accessor's getter and setter provide the desired behavior. For
* non-reflective operations, the illusion is perfect. However, reflective
* operations like `getOwnPropertyDescriptor` see the descriptor of an accessor
* property rather than the descriptor of a data property. At the time of this
* writing, this is the best we know how to do.
*
* To the getter of the accessor we add a property named
* `'originalValue'` whose value is, as it says, the value that the
* data property had before being converted to an accessor property. We add
* this extra property to the getter for two reason:
*
* The harden algorithm walks the own properties reflectively, i.e., with
* `getOwnPropertyDescriptor` semantics, rather than `[[Get]]` semantics. When
* it sees an accessor property, it does not invoke the getter. Rather, it
* proceeds to walk both the getter and setter as part of its transitive
* traversal. Without this extra property, `enablePropertyOverrides` would have
* hidden the original data property value from `harden`, which would be bad.
* Instead, by exposing that value in an own data property on the getter,
* `harden` finds and walks it anyway.
*
* We enable a form of cooperative emulation, giving reflective code an
* opportunity to cooperate in upholding the illusion. When such cooperative
* reflective code sees an accessor property, where the accessor's getter
* has an `originalValue` property, it knows that the getter is
* alleging that it is the result of the `enablePropertyOverrides` conversion
* pattern, so it can decide to cooperatively "pretend" that it sees a data
* property with that value.
*
* @param {Record<string, any>} intrinsics
* @param {'min' | 'moderate' | 'severe'} overrideTaming
* @param {Reporter} reporter
* @param {Iterable<string | symbol>} [overrideDebug]
*/
export default function enablePropertyOverrides(
intrinsics,
overrideTaming,
{ warn },
overrideDebug = [],
) {
const debugProperties = new Set(overrideDebug);
function enable(path, obj, prop, desc) {
if ('value' in desc && desc.configurable) {
const { value } = desc;
const isDebug = setHas(debugProperties, prop);
// We use concise method syntax to be `this` sensitive, but still
// omit a prototype property or [[Construct]] behavior.
// @ts-expect-error We know there is an accessor descriptor there
const { get: getter, set: setter } = getOwnPropertyDescriptor(
{
get [prop]() {
return value;
},
set [prop](newValue) {
if (obj === this) {
throw TypeError(
`Cannot assign to read only property '${String(
prop,
)}' of '${path}'`,
);
}
if (objectHasOwnProperty(this, prop)) {
this[prop] = newValue;
} else {
if (isDebug) {
warn(TypeError(`Override property ${prop}`));
}
defineProperty(this, prop, {
value: newValue,
writable: true,
enumerable: true,
configurable: true,
});
}
},
},
prop,
);
defineProperty(getter, 'originalValue', {
value,
writable: false,
enumerable: false,
configurable: false,
});
defineProperty(obj, prop, {
get: getter,
set: setter,
enumerable: desc.enumerable,
configurable: desc.configurable,
});
}
}
function enableProperty(path, obj, prop) {
const desc = getOwnPropertyDescriptor(obj, prop);
if (!desc) {
return;
}
enable(path, obj, prop, desc);
}
function enableAllProperties(path, obj) {
const descs = getOwnPropertyDescriptors(obj);
if (!descs) {
return;
}
// TypeScript does not allow symbols to be used as indexes because it
// cannot recokon types of symbolized properties.
arrayForEach(ownKeys(descs), prop => enable(path, obj, prop, descs[prop]));
}
function enableProperties(path, obj, plan) {
for (const prop of ownKeys(plan)) {
const desc = getOwnPropertyDescriptor(obj, prop);
if (!desc || desc.get || desc.set) {
// No not a value property, nothing to do.
// eslint-disable-next-line no-continue
continue;
}
// In case `prop` is a symbol, we first coerce it with `String`,
// purely for diagnostic purposes.
const subPath = `${path}.${String(prop)}`;
const subPlan = plan[prop];
if (subPlan === true) {
enableProperty(subPath, obj, prop);
} else if (subPlan === '*') {
enableAllProperties(subPath, desc.value);
} else if (isObject(subPlan)) {
enableProperties(subPath, desc.value, subPlan);
} else {
throw TypeError(`Unexpected override enablement plan ${subPath}`);
}
}
}
let plan;
switch (overrideTaming) {
case 'min': {
plan = minEnablements;
break;
}
case 'moderate': {
plan = moderateEnablements;
break;
}
case 'severe': {
plan = severeEnablements;
break;
}
default: {
throw TypeError(`unrecognized overrideTaming ${overrideTaming}`);
}
}
// Do the repair.
enableProperties('root', intrinsics, plan);
}