UNPKG

ses

Version:

Hardened JavaScript for Fearless Cooperation

276 lines (249 loc) 9.04 kB
// Adapted from SES/Caja - Copyright (C) 2011 Google Inc. // Copyright (C) 2018 Agoric // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // based upon: // 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 // then copied from proposal-frozen-realms deep-freeze.js // then copied from SES/src/bundle/deepFreeze.js // @ts-check import { Set, String, TypeError, WeakSet, globalThis, apply, arrayForEach, defineProperty, freeze, getOwnPropertyDescriptor, getOwnPropertyDescriptors, getPrototypeOf, isInteger, isObject, objectHasOwnProperty, ownKeys, preventExtensions, setAdd, setForEach, setHas, toStringTagSymbol, typedArrayPrototype, weaksetAdd, weaksetHas, FERAL_STACK_GETTER, FERAL_STACK_SETTER, isError, } from './commons.js'; import { assert } from './error/assert.js'; /** * @import {Harden} from '../types.js' */ // Obtain the string tag accessor of of TypedArray so we can indirectly use the // TypedArray brand check it employs. const typedArrayToStringTag = getOwnPropertyDescriptor( typedArrayPrototype, toStringTagSymbol, ); assert(typedArrayToStringTag); const getTypedArrayToStringTag = typedArrayToStringTag.get; assert(getTypedArrayToStringTag); // Exported for tests. /** * Duplicates packages/marshal/src/helpers/passStyle-helpers.js to avoid a dependency. * * @param {unknown} object */ export const isTypedArray = object => { // The object must pass a brand check or toStringTag will return undefined. const tag = apply(getTypedArrayToStringTag, object, []); return tag !== undefined; }; /** * Tests if a property key is an integer-valued canonical numeric index. * https://tc39.es/ecma262/#sec-canonicalnumericindexstring * * @param {string | symbol} propertyKey */ const isCanonicalIntegerIndexString = propertyKey => { const n = +String(propertyKey); return isInteger(n) && String(n) === propertyKey; }; /** * @template T * @param {ArrayLike<T>} array */ const freezeTypedArray = array => { preventExtensions(array); // Downgrade writable expandos to readonly, even if non-configurable. // We get each descriptor individually rather than using // getOwnPropertyDescriptors in order to fail safe when encountering // an obscure GraalJS issue where getOwnPropertyDescriptor returns // undefined for a property that does exist. arrayForEach(ownKeys(array), (/** @type {string | symbol} */ name) => { const desc = getOwnPropertyDescriptor(array, name); assert(desc); // TypedArrays are integer-indexed exotic objects, which define special // treatment for property names in canonical numeric form: // integers in range are permanently writable and non-configurable. // https://tc39.es/ecma262/#sec-integer-indexed-exotic-objects // // This is analogous to the data of a hardened Map or Set, // so we carve out this exceptional behavior but make all other // properties non-configurable. if (!isCanonicalIntegerIndexString(name)) { defineProperty(array, name, { ...desc, writable: false, configurable: false, }); } }); }; /** * Create a `harden` function. * * @returns {Harden} */ export const makeHardener = () => { // Use a native hardener if possible. if (typeof globalThis.harden === 'function') { const safeHarden = globalThis.harden; return safeHarden; } const hardened = new WeakSet(); const { harden } = { /** * @template T * @param {T} root * @returns {T} */ harden(root) { const toFreeze = new Set(); // If val is something we should be freezing but aren't yet, // add it to toFreeze. /** * @param {any} val */ function enqueue(val) { if (!isObject(val)) { // ignore primitives return; } const type = typeof val; if (type !== 'object' && type !== 'function') { // future proof: break until someone figures out what it should do throw TypeError(`Unexpected typeof: ${type}`); } if (weaksetHas(hardened, val) || setHas(toFreeze, val)) { // Ignore if this is an exit, or we've already visited it return; } // console.warn(`adding ${val} to toFreeze`, val); setAdd(toFreeze, val); } /** * @param {any} obj */ const baseFreezeAndTraverse = obj => { // Now freeze the object to ensure reactive // objects such as proxies won't add properties // during traversal, before they get frozen. // Object are verified before being enqueued, // therefore this is a valid candidate. // Throws if this fails (strict mode). // Also throws if the object is an ArrayBuffer or any TypedArray. if (isTypedArray(obj)) { freezeTypedArray(obj); } else { freeze(obj); } // we rely upon certain commitments of Object.freeze and proxies here // get stable/immutable outbound links before a Proxy has a chance to do // something sneaky. const descs = getOwnPropertyDescriptors(obj); const proto = getPrototypeOf(obj); enqueue(proto); arrayForEach(ownKeys(descs), (/** @type {string | symbol} */ name) => { // The 'name' may be a symbol, and TypeScript doesn't like us to // index arbitrary symbols on objects, so we pretend they're just // strings. const desc = descs[/** @type {string} */ (name)]; // getOwnPropertyDescriptors is guaranteed to return well-formed // descriptors, but they still inherit from Object.prototype. If // someone has poisoned Object.prototype to add 'value' or 'get' // properties, then a simple 'if ("value" in desc)' or 'desc.value' // test could be confused. We use hasOwnProperty to be sure about // whether 'value' is present or not, which tells us for sure that // this is a data property. if (objectHasOwnProperty(desc, 'value')) { enqueue(desc.value); } else { enqueue(desc.get); enqueue(desc.set); } }); }; const freezeAndTraverse = FERAL_STACK_GETTER === undefined && FERAL_STACK_SETTER === undefined ? // On platforms without v8's error own stack accessor problem, // don't pay for any extra overhead. baseFreezeAndTraverse : obj => { if (isError(obj)) { // Only pay the overhead if it first passes this cheap isError // check. Otherwise, it will be unrepaired, but won't be judged // to be a passable error anyway, so will not be unsafe. const stackDesc = getOwnPropertyDescriptor(obj, 'stack'); if ( stackDesc && stackDesc.get === FERAL_STACK_GETTER && stackDesc.configurable ) { // Can only repair if it is configurable. Otherwise, leave // unrepaired, in which case it will not be judged passable, // avoiding a safety problem. defineProperty(obj, 'stack', { // NOTE: Calls getter during harden, which seems dangerous. // But we're only calling the problematic getter whose // hazards we think we understand. // @ts-expect-error TS should know FERAL_STACK_GETTER // cannot be `undefined` here. // See https://github.com/endojs/endo/pull/2232#discussion_r1575179471 value: apply(FERAL_STACK_GETTER, obj, []), }); } } return baseFreezeAndTraverse(obj); }; const dequeue = () => { // New values added before forEach() has finished will be visited. setForEach(toFreeze, freezeAndTraverse); }; /** @param {any} value */ const markHardened = value => { weaksetAdd(hardened, value); }; const commit = () => { setForEach(toFreeze, markHardened); }; enqueue(root); dequeue(); // console.warn("toFreeze set:", toFreeze); commit(); return root; }, }; return harden; };