From 03791e1a1e1949832374aeb0ba3d0c4f78be666d Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Thu, 2 Jul 2020 15:15:25 -0700 Subject: [PATCH] fix: Massive intrinsic reform. Start vs other compartments. --- packages/ses/src/compartment-shim.js | 307 ++++--- packages/ses/src/enablements.js | 28 +- packages/ses/src/evaluate.js | 7 +- packages/ses/src/get-anonymous-intrinsics.js | 49 +- packages/ses/src/get-named-intrinsic.js | 20 - packages/ses/src/global-object.js | 199 ++--- packages/ses/src/intrinsic-names.js | 152 ---- packages/ses/src/intrinsics-global.js | 129 --- packages/ses/src/intrinsics.js | 218 +++-- packages/ses/src/lockdown-shim.js | 96 ++- packages/ses/src/make-eval-function.js | 14 +- packages/ses/src/make-evaluate-factory.js | 9 +- packages/ses/src/make-function-constructor.js | 13 +- packages/ses/src/module-instance.js | 3 - packages/ses/src/realm-rec.js | 32 - packages/ses/src/scope-handler.js | 17 +- packages/ses/src/tame-date-constructor.js | 104 +++ packages/ses/src/tame-error-constructor.js | 80 ++ .../ses/src/tame-function-constructors.js | 48 +- packages/ses/src/tame-global-date-object.js | 92 -- packages/ses/src/tame-global-error-object.js | 293 ------- packages/ses/src/tame-global-math-object.js | 46 - .../ses/src/tame-global-reg-exp-object.js | 54 -- packages/ses/src/tame-locale-methods.js | 30 +- packages/ses/src/tame-math-object.js | 23 + packages/ses/src/tame-regexp-constructor.js | 49 ++ packages/ses/src/tame-v8-error-constructor.js | 187 ++++ packages/ses/src/whitelist-intrinsics.js | 101 ++- packages/ses/src/whitelist.js | 811 ++++++++++++------ .../{src => test}/check-anon-intrinsics.js | 52 +- .../ses/{src => test}/check-intrinsics.js | 0 .../ses/test/compartment-instance.test.js | 2 +- .../ses/test/compartment-prototype.test.js | 2 +- .../test/enable-property-overrides.test.js | 12 +- packages/ses/test/evaluate.test.js | 28 +- .../ses/test/get-global-intrinsics.test.js | 8 +- packages/ses/test/get-intrinsics.test.js | 117 --- .../ses/test/global-object-properties.test.js | 2 +- packages/ses/test/global-object.test.js | 35 +- packages/ses/test/identity-continuity.test.js | 13 +- packages/ses/test/lockdown-allow.test.js | 3 +- packages/ses/test/lockdown.test.js | 4 +- packages/ses/test/make-eval-function.test.js | 6 +- .../ses/test/make-evaluate-factory.test.js | 5 +- .../test/make-function-constructor.test.js | 3 +- packages/ses/test/realm-rec.test.js | 27 - packages/ses/test/scope-handler.test.js | 34 +- .../test/static-module-record-unit.test.js | 29 + packages/ses/test/tame-date-allow.test.js | 43 - packages/ses/test/tame-date-unit.test.js | 57 ++ packages/ses/test/tame-date.test.js | 31 +- packages/ses/test/tame-error-allow.test.js | 28 - packages/ses/test/tame-error.test.js | 28 - ...ors.test.js => tame-function-unit.test.js} | 0 .../tame-global-date-object-allow.test.js | 38 - .../ses/test/tame-global-date-object.test.js | 38 - .../tame-global-error-object-allow.test.js | 27 - .../ses/test/tame-global-error-object.test.js | 27 - .../tame-global-math-object-allow.test.js | 18 - .../ses/test/tame-global-math-object.test.js | 18 - .../tame-global-reg-exp-object-allow.test.js | 72 -- .../test/tame-locale-methods-allow.test.js | 25 - .../test/tame-locale-methods-unsafe.test.js | 28 + packages/ses/test/tame-locale-methods.test.js | 27 +- packages/ses/test/tame-math-allow.test.js | 21 - packages/ses/test/tame-math-unit.test.js | 23 + packages/ses/test/tame-math.test.js | 16 +- ...bject.test.js => tame-regexp-unit.test.js} | 56 +- packages/ses/test/tame-rexexp-allow.test.js | 25 - packages/ses/test/tame-rexexp.test.js | 8 +- packages/ses/test/tame-v8-error-unit.test.js | 22 + ...n.test.js => tame-v8-error-unsafe.test.js} | 2 +- packages/ses/test/tame-v8-error.test.js | 28 + .../ses/test/whitelist-intrinsics.test.js | 23 +- .../ses/test262/tame-global-date-object.js | 6 +- .../ses/test262/tame-global-error-object.js | 13 +- .../ses/test262/tame-global-math-object.js | 6 +- .../ses/test262/tame-global-regexp-object.js | 14 +- 78 files changed, 2016 insertions(+), 2345 deletions(-) delete mode 100644 packages/ses/src/get-named-intrinsic.js delete mode 100644 packages/ses/src/intrinsic-names.js delete mode 100644 packages/ses/src/intrinsics-global.js delete mode 100644 packages/ses/src/realm-rec.js create mode 100644 packages/ses/src/tame-date-constructor.js create mode 100644 packages/ses/src/tame-error-constructor.js delete mode 100644 packages/ses/src/tame-global-date-object.js delete mode 100644 packages/ses/src/tame-global-error-object.js delete mode 100644 packages/ses/src/tame-global-math-object.js delete mode 100644 packages/ses/src/tame-global-reg-exp-object.js create mode 100644 packages/ses/src/tame-math-object.js create mode 100644 packages/ses/src/tame-regexp-constructor.js create mode 100644 packages/ses/src/tame-v8-error-constructor.js rename packages/ses/{src => test}/check-anon-intrinsics.js (72%) rename packages/ses/{src => test}/check-intrinsics.js (100%) delete mode 100644 packages/ses/test/get-intrinsics.test.js delete mode 100644 packages/ses/test/realm-rec.test.js create mode 100644 packages/ses/test/static-module-record-unit.test.js delete mode 100644 packages/ses/test/tame-date-allow.test.js create mode 100644 packages/ses/test/tame-date-unit.test.js delete mode 100644 packages/ses/test/tame-error-allow.test.js delete mode 100644 packages/ses/test/tame-error.test.js rename packages/ses/test/{tame-function-constructors.test.js => tame-function-unit.test.js} (100%) delete mode 100644 packages/ses/test/tame-global-date-object-allow.test.js delete mode 100644 packages/ses/test/tame-global-date-object.test.js delete mode 100644 packages/ses/test/tame-global-error-object-allow.test.js delete mode 100644 packages/ses/test/tame-global-error-object.test.js delete mode 100644 packages/ses/test/tame-global-math-object-allow.test.js delete mode 100644 packages/ses/test/tame-global-math-object.test.js delete mode 100644 packages/ses/test/tame-global-reg-exp-object-allow.test.js delete mode 100644 packages/ses/test/tame-locale-methods-allow.test.js create mode 100644 packages/ses/test/tame-locale-methods-unsafe.test.js delete mode 100644 packages/ses/test/tame-math-allow.test.js create mode 100644 packages/ses/test/tame-math-unit.test.js rename packages/ses/test/{tame-global-reg-exp-object.test.js => tame-regexp-unit.test.js} (50%) delete mode 100644 packages/ses/test/tame-rexexp-allow.test.js create mode 100644 packages/ses/test/tame-v8-error-unit.test.js rename packages/ses/test/{error-manipulation.test.js => tame-v8-error-unsafe.test.js} (98%) create mode 100644 packages/ses/test/tame-v8-error.test.js diff --git a/packages/ses/src/compartment-shim.js b/packages/ses/src/compartment-shim.js index beef5cb409..e3e3d590c7 100644 --- a/packages/ses/src/compartment-shim.js +++ b/packages/ses/src/compartment-shim.js @@ -13,14 +13,23 @@ import * as babel from '@agoric/babel-standalone'; // Both produce: // Error: 'default' is not exported by .../@agoric/babel-standalone/babel.js import { makeModuleAnalyzer } from '@agoric/transform-module'; -import { assign, entries, getOwnPropertyNames, freeze } from './commons.js'; -import { createGlobalObject } from './global-object.js'; +import { + assign, + defineProperties, + entries, + freeze, + getOwnPropertyNames, + keys, +} from './commons.js'; +// eslint-disable-next-line import/no-cycle +import { initGlobalObject } from './global-object.js'; import { performEval } from './evaluate.js'; -import { getCurrentRealmRec } from './realm-rec.js'; import { load } from './module-load.js'; import { link } from './module-link.js'; import { getDeferredExports } from './module-proxy.js'; import { isValidIdentifierName } from './scope-constants.js'; +import { sharedGlobalPropertyNames } from './whitelist.js'; +import { getGlobalIntrinsics } from './intrinsics.js'; // q, for quoting strings. const q = JSON.stringify; @@ -37,27 +46,66 @@ const moduleAnalyses = new WeakMap(); * StaticModuleRecord captures the effort of parsing and analyzing module text * so a cache of StaticModuleRecords may be shared by multiple Compartments. */ -export class StaticModuleRecord { - constructor(string, url) { - const analysis = analyzeModule({ string, url }); +export function StaticModuleRecord(string, url) { + if (new.target === undefined) { + return new StaticModuleRecord(string, url); + } - this.imports = Object.keys(analysis.imports).sort(); + const analysis = analyzeModule({ string, url }); - freeze(this); - freeze(this.imports); + this.imports = keys(analysis.imports).sort(); - moduleAnalyses.set(this, analysis); - } + freeze(this); + freeze(this.imports); + + moduleAnalyses.set(this, analysis); +} + +// It is not clear that +// `StaticModuleRecord.prototype.constructor` needs to be the +// useless `InertStaticModuleRecord` rather than +// `StaticModuleRecord` itself. The reason we're starting off +// extra caution is that `StaticModuleRecord` would be the only +// remaining universally shared primordial reachable by navigation +// that can turn strings of code into a representation closer to +// code execution. The others, `eval`, `Function`, and `Compartment` +// are already protected, and only `Compartment` can turn a +// `StaticModuleRecord` into execution, which is why it would +// probably be safe. However, since this extra caution has a tiny +// cost, I'd rather start out more restrictive, maintaining the option +// to loosen the rule over time. +// +// eslint-disable-next-line no-shadow +const InertStaticModuleRecord = function StaticModuleRecord(_string, _url) { + throw new TypeError('Not available'); +}; - // eslint-disable-next-line class-methods-use-this +const StaticModuleRecordPrototype = { + constructor: InertStaticModuleRecord, toString() { return '[object StaticModuleRecord]'; - } + }, +}; - static toString() { +const staticModuleRecordStaticMethods = { + toString() { return 'function StaticModuleRecord() { [shim code] }'; - } -} + }, +}; + +defineProperties(StaticModuleRecord, { + prototype: { value: StaticModuleRecordPrototype }, + toString: { + value: staticModuleRecordStaticMethods.toString, + writable: true, + enumerable: false, + configurable: true, + }, +}); + +defineProperties(InertStaticModuleRecord, { + prototype: { value: StaticModuleRecordPrototype }, +}); // privateFields captures the private state for each compartment. const privateFields = new WeakMap(); @@ -83,13 +131,119 @@ const assertModuleHooks = compartment => { } }; -/** - * Compartment() - * The Compartment constructor is a global. A host that wants to execute - * code in a context bound to a new global creates a new compartment. - */ -export class Compartment { - constructor(endowments = {}, modules = {}, options = {}) { +const InertCompartment = function Compartment( + _endowments = {}, + _modules = {}, + _options = {}, +) { + throw new TypeError('Not available'); +}; + +const CompartmentPrototype = { + constructor: InertCompartment, + get globalThis() { + return privateFields.get(this).globalObject; + }, + + /** + * @param {string} source is a JavaScript program grammar construction. + * @param {{ + * transforms: Array, + * sloppyGlobalsMode: bool, + * }} options. + */ + evaluate(source, options = {}) { + // Perform this check first to avoid unecessary sanitizing. + if (typeof source !== 'string') { + throw new TypeError('first argument of evaluate() must be a string'); + } + + // Extract options, and shallow-clone transforms. + const { transforms = [], sloppyGlobalsMode = false } = options; + const localTransforms = [...transforms]; + + const { + globalTransforms, + globalObject, + globalLexicals, + } = privateFields.get(this); + + return performEval(source, globalObject, globalLexicals, { + globalTransforms, + localTransforms, + sloppyGlobalsMode, + }); + }, + + module(specifier) { + if (typeof specifier !== 'string') { + throw new TypeError('first argument of module() must be a string'); + } + + assertModuleHooks(this); + + const { exportsProxy } = getDeferredExports( + this, + privateFields.get(this), + moduleAliases, + specifier, + ); + + return exportsProxy; + }, + + async import(specifier) { + if (typeof specifier !== 'string') { + throw new TypeError('first argument of import() must be a string'); + } + + assertModuleHooks(this); + + return load(privateFields, moduleAnalyses, this, specifier).then(() => { + const namespace = this.importNow(specifier); + return { namespace }; + }); + }, + + importNow(specifier) { + if (typeof specifier !== 'string') { + throw new TypeError('first argument of importNow() must be a string'); + } + + assertModuleHooks(this); + + const moduleInstance = link(privateFields, moduleAliases, this, specifier); + moduleInstance.execute(); + return moduleInstance.exportsProxy; + }, + + toString() { + return '[object Compartment]'; + }, +}; +const compartmentStaticMethods = { + toString() { + return 'function Compartment() { [shim code] }'; + }, +}; + +defineProperties(InertCompartment, { + prototype: { value: CompartmentPrototype }, + toString: { + value: compartmentStaticMethods.toString, + writable: true, + enumerable: false, + configurable: true, + }, +}); + +export const makeCompartmentConstructor = intrinsics => { + /** + * Compartment() + * Each Compartment constructor is a global. A host that wants to execute + * code in a context bound to a new global creates a new compartment. + */ + function Compartment(endowments = {}, modules = {}, options = {}) { // Extract options, and shallow-clone transforms. const { transforms = [], @@ -99,8 +253,8 @@ export class Compartment { } = options; const globalTransforms = [...transforms]; - const realmRec = getCurrentRealmRec(); - const globalObject = createGlobalObject(realmRec, { + const globalObject = {}; + initGlobalObject(globalObject, intrinsics, sharedGlobalPropertyNames, { globalTransforms, }); @@ -128,7 +282,8 @@ export class Compartment { // Modules from other components. aliases.set(specifier, alias); } else { - // TODO create and link a synthetic module instance from the given namespace object. + // TODO create and link a synthetic module instance from the given + // namespace object. throw ReferenceError( `Cannot map module ${q( specifier, @@ -170,89 +325,19 @@ export class Compartment { }); } - get globalThis() { - return privateFields.get(this).globalObject; - } - - /** - * @param {string} source is a JavaScript program grammar construction. - * @param {{ - * transforms: Array, - * sloppyGlobalsMode: bool, - * }} options. - */ - evaluate(source, options = {}) { - // Perform this check first to avoid unecessary sanitizing. - if (typeof source !== 'string') { - throw new TypeError('first argument of evaluate() must be a string'); - } - - // Extract options, and shallow-clone transforms. - const { transforms = [], sloppyGlobalsMode = false } = options; - const localTransforms = [...transforms]; - - const { - globalTransforms, - globalObject, - globalLexicals, - } = privateFields.get(this); - const realmRec = getCurrentRealmRec(); - - return performEval(realmRec, source, globalObject, globalLexicals, { - globalTransforms, - localTransforms, - sloppyGlobalsMode, - }); - } - - module(specifier) { - if (typeof specifier !== 'string') { - throw new TypeError('first argument of module() must be a string'); - } - - assertModuleHooks(this); - - const { exportsProxy } = getDeferredExports( - this, - privateFields.get(this), - moduleAliases, - specifier, - ); - - return exportsProxy; - } - - async import(specifier) { - if (typeof specifier !== 'string') { - throw new TypeError('first argument of import() must be a string'); - } - - assertModuleHooks(this); - - return load(privateFields, moduleAnalyses, this, specifier).then(() => { - const namespace = this.importNow(specifier); - return { namespace }; - }); - } - - importNow(specifier) { - if (typeof specifier !== 'string') { - throw new TypeError('first argument of importNow() must be a string'); - } - - assertModuleHooks(this); - - const moduleInstance = link(privateFields, moduleAliases, this, specifier); - moduleInstance.execute(); - return moduleInstance.exportsProxy; - } - - // eslint-disable-next-line class-methods-use-this - toString() { - return '[object Compartment]'; - } + defineProperties(Compartment, { + prototype: { value: CompartmentPrototype }, + toString: { + value: compartmentStaticMethods.toString, + writable: true, + enumerable: false, + configurable: true, + }, + }); + + return Compartment; +}; - static toString() { - return 'function Compartment() { [shim code] }'; - } -} +export const Compartment = makeCompartmentConstructor( + getGlobalIntrinsics(globalThis), +); diff --git a/packages/ses/src/enablements.js b/packages/ses/src/enablements.js index d483fb06f3..18cc77c189 100644 --- a/packages/ses/src/enablements.js +++ b/packages/ses/src/enablements.js @@ -57,11 +57,11 @@ */ export default { - ObjectPrototype: '*', + '%ObjectPrototype%': '*', - ArrayPrototype: '*', + '%ArrayPrototype%': '*', - FunctionPrototype: { + '%FunctionPrototype%': { constructor: true, // set by "regenerator-runtime" bind: true, // set by "underscore" apply: true, // set by "tape" @@ -69,50 +69,50 @@ export default { toString: true, }, - ErrorPrototype: { + '%ErrorPrototype%': { constructor: true, // set by "fast-json-patch" message: true, name: true, // set by "precond" toString: true, // set by "bluebird" }, - TypeErrorPrototype: { + '%TypeErrorPrototype%': { constructor: true, // set by "readable-stream" message: true, // set by "tape" name: true, // set by "readable-stream" }, - SyntaxErrorPrototype: { + '%SyntaxErrorPrototype%': { message: true, // to match TypeErrorPrototype.message }, - RangeErrorPrototype: { + '%RangeErrorPrototype%': { message: true, // to match TypeErrorPrototype.message }, - URIErrorPrototype: { + '%URIErrorPrototype%': { message: true, // to match TypeErrorPrototype.message }, - EvalErrorPrototype: { + '%EvalErrorPrototype%': { message: true, // to match TypeErrorPrototype.message }, - ReferenceErrorPrototype: { + '%ReferenceErrorPrototype%': { message: true, // to match TypeErrorPrototype.message }, - PromisePrototype: { + '%PromisePrototype%': { constructor: true, // set by "core-js" }, - TypedArrayPrototype: '*', + '%TypedArrayPrototype%': '*', - Generator: { + '%Generator%': { constructor: true, name: true, toString: true, }, - IteratorPrototype: '*', + '%IteratorPrototype%': '*', }; diff --git a/packages/ses/src/evaluate.js b/packages/ses/src/evaluate.js index d2b61a80c3..00b18e9b69 100644 --- a/packages/ses/src/evaluate.js +++ b/packages/ses/src/evaluate.js @@ -9,12 +9,11 @@ import { applyTransforms, mandatoryTransforms } from './transforms.js'; import { makeEvaluateFactory } from './make-evaluate-factory.js'; /** - * makeEvalFunction() + * performEval() * The low-level operation used by all evaluators: * eval(), Function(), Evalutator.prototype.evaluate(). */ export function performEval( - realmRec, source, globalObject, localObject = {}, @@ -32,14 +31,14 @@ export function performEval( mandatoryTransforms, ]); - const scopeHandler = createScopeHandler(realmRec, globalObject, localObject, { + const scopeHandler = createScopeHandler(globalObject, localObject, { sloppyGlobalsMode, }); const scopeProxyRevocable = proxyRevocable(immutableObject, scopeHandler); // Ensure that "this" resolves to the scope proxy. const constants = getScopeConstants(globalObject, localObject); - const evaluateFactory = makeEvaluateFactory(realmRec, constants); + const evaluateFactory = makeEvaluateFactory(constants); const evaluate = apply(evaluateFactory, scopeProxyRevocable.proxy, []); scopeHandler.useUnsafeEvaluator = true; diff --git a/packages/ses/src/get-anonymous-intrinsics.js b/packages/ses/src/get-anonymous-intrinsics.js index 4531a9e35a..b34e0caa18 100644 --- a/packages/ses/src/get-anonymous-intrinsics.js +++ b/packages/ses/src/get-anonymous-intrinsics.js @@ -1,4 +1,5 @@ import { getOwnPropertyDescriptor, getPrototypeOf } from './commons.js'; +import { Compartment, StaticModuleRecord } from './compartment-shim.js'; /** * Object.getConstructorOf() @@ -14,7 +15,7 @@ function getConstructorOf(obj) { * traversal from the global object. */ export function getAnonymousIntrinsics() { - const FunctionPrototypeConstructor = Function.prototype.constructor; + const InertFunction = Function.prototype.constructor; const SymbolIterator = (typeof Symbol && Symbol.iterator) || '@@iterator'; const SymbolMatchAll = (typeof Symbol && Symbol.matchAll) || '@@matchAll'; @@ -61,7 +62,8 @@ export function getAnonymousIntrinsics() { // 25.2.1 The GeneratorFunction Constructor - function* GeneratorFunctionInstance() {} // eslint-disable-line no-empty-function + // eslint-disable-next-line no-empty-function + function* GeneratorFunctionInstance() {} const GeneratorFunction = getConstructorOf(GeneratorFunctionInstance); // 25.2.3 Properties of the GeneratorFunction Prototype Object @@ -70,7 +72,8 @@ export function getAnonymousIntrinsics() { // 25.3.1 The AsyncGeneratorFunction Constructor - async function* AsyncGeneratorFunctionInstance() {} // eslint-disable-line no-empty-function + // eslint-disable-next-line no-empty-function + async function* AsyncGeneratorFunctionInstance() {} const AsyncGeneratorFunction = getConstructorOf( AsyncGeneratorFunctionInstance, ); @@ -83,28 +86,32 @@ export function getAnonymousIntrinsics() { // 25.7.1 The AsyncFunction Constructor - async function AsyncFunctionInstance() {} // eslint-disable-line no-empty-function + // eslint-disable-next-line no-empty-function + async function AsyncFunctionInstance() {} const AsyncFunction = getConstructorOf(AsyncFunctionInstance); - // VALIDATION + const InertCompartment = Compartment.prototype.constructor; + const InertStaticModuleRecord = StaticModuleRecord.prototype.constructor; const intrinsics = { - FunctionPrototypeConstructor, - ArrayIteratorPrototype, - AsyncFunction, - AsyncGenerator, - AsyncGeneratorFunction, - AsyncGeneratorPrototype, - AsyncIteratorPrototype, - Generator, - GeneratorFunction, - IteratorPrototype, - MapIteratorPrototype, - RegExpStringIteratorPrototype, - SetIteratorPrototype, - StringIteratorPrototype, - ThrowTypeError, - TypedArray, + '%InertFunction%': InertFunction, + '%ArrayIteratorPrototype%': ArrayIteratorPrototype, + '%InertAsyncFunction%': AsyncFunction, + '%AsyncGenerator%': AsyncGenerator, + '%InertAsyncGeneratorFunction%': AsyncGeneratorFunction, + '%AsyncGeneratorPrototype%': AsyncGeneratorPrototype, + '%AsyncIteratorPrototype%': AsyncIteratorPrototype, + '%Generator%': Generator, + '%InertGeneratorFunction%': GeneratorFunction, + '%IteratorPrototype%': IteratorPrototype, + '%MapIteratorPrototype%': MapIteratorPrototype, + '%RegExpStringIteratorPrototype%': RegExpStringIteratorPrototype, + '%SetIteratorPrototype%': SetIteratorPrototype, + '%StringIteratorPrototype%': StringIteratorPrototype, + '%ThrowTypeError%': ThrowTypeError, + '%TypedArray%': TypedArray, + '%InertCompartment%': InertCompartment, + '%InertStaticModuleRecord%': InertStaticModuleRecord, }; return intrinsics; diff --git a/packages/ses/src/get-named-intrinsic.js b/packages/ses/src/get-named-intrinsic.js deleted file mode 100644 index 0ceb5d7dc7..0000000000 --- a/packages/ses/src/get-named-intrinsic.js +++ /dev/null @@ -1,20 +0,0 @@ -import { assert } from './assert.js'; -import { getOwnPropertyDescriptor } from './commons.js'; - -/** - * getNamedIntrinsic() - * Get the intrinsic from the global object. - */ -export function getNamedIntrinsic(root, name) { - // Assumption: the intrinsic name matches a global object with the same name. - const desc = getOwnPropertyDescriptor(root, name); - - // Abort if an accessor is found on the object instead of a data property. - // We should never get into this non standard situation. - assert( - !('get' in desc || 'set' in desc), - `unexpected accessor on global property: ${name}`, - ); - - return desc.value; -} diff --git a/packages/ses/src/global-object.js b/packages/ses/src/global-object.js index 83d13f2b65..f6fcec8f60 100644 --- a/packages/ses/src/global-object.js +++ b/packages/ses/src/global-object.js @@ -1,160 +1,73 @@ -import { assert } from './assertions.js'; -import { defineProperties, objectHasOwnProperty } from './commons.js'; +import { defineProperty, objectHasOwnProperty, entries } from './commons.js'; import { makeEvalFunction } from './make-eval-function.js'; import { makeFunctionConstructor } from './make-function-constructor.js'; +import { constantProperties, universalPropertyNames } from './whitelist.js'; +// eslint-disable-next-line import/no-cycle +import { makeCompartmentConstructor } from './compartment-shim.js'; /** - * globalPropertyNames - * Properties of the global object. - */ -const globalPropertyNames = [ - // *** 18.2 Function Properties of the Global Object - - 'eval', - 'isFinite', - 'isNaN', - 'parseFloat', - 'parseInt', - - 'decodeURI', - 'decodeURIComponent', - 'encodeURI', - 'encodeURIComponent', - - // *** 18.3 Constructor Properties of the Global Object - - 'Array', - 'ArrayBuffer', - 'Boolean', - 'DataView', - 'Date', - 'Error', - 'EvalError', - 'Float32Array', - 'Float64Array', - 'Function', - 'Int8Array', - 'Int16Array', - 'Int32Array', - 'Map', - 'Number', - 'Object', - 'Promise', - 'Proxy', - 'RangeError', - 'ReferenceError', - 'RegExp', - 'Set', - // 'SharedArrayBuffer' // removed on Jan 5, 2018 - 'String', - 'Symbol', - 'SyntaxError', - 'TypeError', - 'Uint8Array', - 'Uint8ClampedArray', - 'Uint16Array', - 'Uint32Array', - 'URIError', - 'WeakMap', - 'WeakSet', - - // *** 18.4 Other Properties of the Global Object - - // 'Atomics', // removed on Jan 5, 2018 - 'JSON', - 'Math', - 'Reflect', - - // *** Annex B - - 'escape', - 'unescape', - - // ESNext - - 'globalThis', - 'Compartment', - 'harden', -]; - -/** - * createGlobalObject() + * initGlobalObject() * Create new global object using a process similar to ECMA specifications - * (portions of SetRealmGlobalObject and SetDefaultGlobalBindings). The new - * global object is not part of the realm record. + * (portions of SetRealmGlobalObject and SetDefaultGlobalBindings). + * `newGlobalPropertyNames` should be either `initialGlobalPropertyNames` or + * `sharedGlobalPropertyNames`. */ -export function createGlobalObject(realmRec, { globalTransforms }) { - const globalObject = {}; - - // Immutable properties. Those values are shared between all realms. - // *** 18.1 Value Properties of the Global Object - const descs = { - Infinity: { - value: Infinity, - enumerable: false, - }, - NaN: { - value: NaN, +export function initGlobalObject( + globalObject, + intrinsics, + newGlobalPropertyNames, + { globalTransforms }, +) { + for (const [name, constant] of entries(constantProperties)) { + defineProperty(globalObject, name, { + value: constant, + writable: false, enumerable: false, - }, - undefined: { - value: undefined, - enumerable: false, - }, - }; + configurable: false, + }); + } - // *** 18.2, 18.3, 18.4 etc. - for (const name of globalPropertyNames) { - if (!objectHasOwnProperty(realmRec.intrinsics, name)) { - // only create the global if the intrinsic exists. - // eslint-disable-next-line no-continue - continue; + for (const [name, intrinsicName] of entries(universalPropertyNames)) { + if (objectHasOwnProperty(intrinsics, intrinsicName)) { + defineProperty(globalObject, name, { + value: intrinsics[intrinsicName], + writable: true, + enumerable: false, + configurable: true, + }); } + } - let value; - switch (name) { - case 'eval': - // Use an evaluator-specific instance of eval. - value = makeEvalFunction(realmRec, globalObject, { - globalTransforms, - }); - break; - - case 'Function': - // Use an evaluator-specific instance of Function. - value = makeFunctionConstructor(realmRec, globalObject, { - globalTransforms, - }); - break; - - case 'globalThis': - // Use an evaluator-specific circular reference. - value = globalObject; - break; - - default: - value = realmRec.intrinsics[name]; + for (const [name, intrinsicName] of entries(newGlobalPropertyNames)) { + if (objectHasOwnProperty(intrinsics, intrinsicName)) { + defineProperty(globalObject, name, { + value: intrinsics[intrinsicName], + writable: true, + enumerable: false, + configurable: true, + }); } + } - descs[name] = { + const perCompartmentGlobals = { + globalThis: globalObject, + eval: makeEvalFunction(globalObject, { + globalTransforms, + }), + Function: makeFunctionConstructor(globalObject, { + globalTransforms, + }), + Compartment: makeCompartmentConstructor(intrinsics), + }; + + // TODO These should still be tamed according to the whitelist before + // being made available. + for (const [name, value] of entries(perCompartmentGlobals)) { + defineProperty(globalObject, name, { value, - configurable: true, writable: true, enumerable: false, - }; + configurable: true, + }); } - - // Define properties all at once. - defineProperties(globalObject, descs); - - assert( - globalObject.eval !== realmRec.intrinsics.eval, - 'eval on global object', - ); - assert( - globalObject.Function !== realmRec.intrinsics.Function, - 'Function on global object', - ); - - return globalObject; } diff --git a/packages/ses/src/intrinsic-names.js b/packages/ses/src/intrinsic-names.js deleted file mode 100644 index 3634d93149..0000000000 --- a/packages/ses/src/intrinsic-names.js +++ /dev/null @@ -1,152 +0,0 @@ -/** - * intrinsicNames - * The following list contains all intrisics names as defined in the specs, - * except that the leading and trailing '%' characters have been removed. We - * want to design from the specs so we can better track changes to the specs. - */ -export const intrinsicNames = [ - // 6.1.7.4 Well-Known Intrinsic Objects - // Table 8: Well-Known Intrinsic Objects - 'Array', - 'ArrayBuffer', - 'ArrayBufferPrototype', - 'ArrayIteratorPrototype', - 'ArrayPrototype', - // TODO ArrayProto_* - // 'ArrayProto_entries', - // 'ArrayProto_forEach', - // 'ArrayProto_keys', - // 'ArrayProto_values', - // 25.1.4.2 The %AsyncFromSyncIteratorPrototype% Object - // TODO Beleived to not be directly accessible to ECMAScript code. - // 'AsyncFromSyncIteratorPrototype', - 'AsyncFunction', - 'AsyncFunctionPrototype', - 'AsyncGenerator', - 'AsyncGeneratorFunction', - 'AsyncGeneratorPrototype', - 'AsyncIteratorPrototype', - 'Atomics', - 'BigInt', - // TOTO: Missing in the specs. - 'BigIntPrototype', - 'BigInt64Array', - // TOTO: Missing in the specs. - 'BigInt64ArrayPrototype', - 'BigUint64Array', - // TOTO: Missing in the specs. - 'BigUint64ArrayPrototype', - 'Boolean', - 'BooleanPrototype', - 'DataView', - 'DataViewPrototype', - 'Date', - 'DatePrototype', - 'decodeURI', - 'decodeURIComponent', - 'encodeURI', - 'encodeURIComponent', - 'Error', - 'ErrorPrototype', - 'eval', - 'EvalError', - 'EvalErrorPrototype', - 'Float32Array', - 'Float32ArrayPrototype', - 'Float64Array', - 'Float64ArrayPrototype', - // 13.7.5.16.2 The %ForInIteratorPrototype% Object - // Documneted as "never directly accessible to ECMAScript code." - // 'ForInIteratorPrototype', - 'Function', - 'FunctionPrototype', - 'Generator', - 'GeneratorFunction', - 'GeneratorPrototype', - 'Int8Array', - 'Int8ArrayPrototype', - 'Int16Array', - 'Int16ArrayPrototype', - 'Int32Array', - 'Int32ArrayPrototype', - 'isFinite', - 'isNaN', - 'IteratorPrototype', - 'JSON', - // TODO - // 'JSONParse', - // 'JSONStringify', - 'Map', - 'MapIteratorPrototype', - 'MapPrototype', - 'Math', - 'Number', - 'NumberPrototype', - 'Object', - 'ObjectPrototype', - // TODO - // 'ObjProto_toString', - // 'ObjProto_valueOf', - 'parseFloat', - 'parseInt', - 'Promise', - 'PromisePrototype', - // TODO - // 'PromiseProto_then', - // 'Promise_all', - // 'Promise_reject', - // 'Promise_resolve', - 'Proxy', - 'RangeError', - 'RangeErrorPrototype', - 'ReferenceError', - 'ReferenceErrorPrototype', - 'Reflect', - 'RegExp', - 'RegExpPrototype', - 'RegExpStringIteratorPrototype', - 'Set', - 'SetIteratorPrototype', - 'SetPrototype', - 'SharedArrayBuffer', - 'SharedArrayBufferPrototype', - 'String', - 'StringIteratorPrototype', - 'StringPrototype', - 'Symbol', - 'SymbolPrototype', - 'SyntaxError', - 'SyntaxErrorPrototype', - 'ThrowTypeError', - 'TypedArray', - 'TypedArrayPrototype', - 'TypeError', - 'TypeErrorPrototype', - 'Uint8Array', - 'Uint8ArrayPrototype', - 'Uint8ClampedArray', - 'Uint8ClampedArrayPrototype', - 'Uint16Array', - 'Uint16ArrayPrototype', - 'Uint32Array', - 'Uint32ArrayPrototype', - 'URIError', - 'URIErrorPrototype', - 'WeakMap', - 'WeakMapPrototype', - 'WeakSet', - 'WeakSetPrototype', - - // B.2.1 Additional Properties of the Global Object - // Table 87: Additional Well-known Intrinsic Objects - 'escape', - 'unescape', - - // ESNext - 'FunctionPrototypeConstructor', - 'Compartment', - 'CompartmentPrototype', - 'StaticModuleRecord', - 'StaticModuleRecordPrototype', - 'harden', -]; diff --git a/packages/ses/src/intrinsics-global.js b/packages/ses/src/intrinsics-global.js deleted file mode 100644 index 664d6697ea..0000000000 --- a/packages/ses/src/intrinsics-global.js +++ /dev/null @@ -1,129 +0,0 @@ -// The global intrinsics are the root named intrinsics (intrinsics that are -// direct properties of the global object). -// -// getGlobalIntrinsics(): Object -// -// Return a record-like object similar to the [[intrinsics]] slot of the -// realmRec in the ES specifications except for the following simpifications: -// -// - we only returns the intrinsics that correspond to the global object -// properties listed in 18.2, 18.3, or 18.4 of ES specifications. -// -// - we use the name of the associated global object property instead of the -// intrinsic name (usually, ` === '%' + + '%'`). -// -// Assumptions -// -// The intrinsic names correspond to the object names with "%" added as prefix and suffix, i.e. the intrinsic "%Object%" is equal to the global object property "Object". -import { getOwnPropertyDescriptor } from './commons.js'; - -/** - * globalIntrinsicNames - * The following subset contains only the intrinsics that correspond to the - * global object properties listed in 18.2, 18.3, or 18.4 on ES specifications. - */ -const globalIntrinsicNames = [ - // *** 18.1 Value Properties of the Global Object - - // Ignore: those value properties are not intrinsics. - - // *** 18.2 Function Properties of the Global Object - - 'eval', - 'isFinite', - 'isNaN', - 'parseFloat', - 'parseInt', - - 'decodeURI', - 'decodeURIComponent', - 'encodeURI', - 'encodeURIComponent', - - // *** 18.3 Constructor Properties of the Global Object - - 'Array', - 'ArrayBuffer', - 'Boolean', - 'DataView', - 'Date', - 'Error', - 'EvalError', - 'Float32Array', - 'Float64Array', - 'Function', - 'Int8Array', - 'Int16Array', - 'Int32Array', - 'Map', - 'Number', - 'Object', - 'Promise', - 'Proxy', - 'RangeError', - 'ReferenceError', - 'RegExp', - 'Set', - // 'SharedArrayBuffer' // removed on Jan 5, 2018 - 'String', - 'Symbol', - 'SyntaxError', - 'TypeError', - 'Uint8Array', - 'Uint8ClampedArray', - 'Uint16Array', - 'Uint32Array', - 'URIError', - 'WeakMap', - 'WeakSet', - - // *** 18.4 Other Properties of the Global Object - - // 'Atomics', // removed on Jan 5, 2018 - 'JSON', - 'Math', - 'Reflect', - - // *** Annex B - - 'escape', - 'unescape', - - // ESNext - - 'globalThis', - 'Compartment', - 'CompartmentPrototype', - 'StaticModuleRecord', - 'StaticModuleRecordPrototype', - 'harden', -]; - -/** - * getGlobalIntrinsics() - * Return a record-like object similar to the [[intrinsics]] slot of the - * realmRec in the ES specifications except for this simpification: - * - we only return the intrinsics that are own properties of the global object. - * - we use the name of the associated global object property - * (usually, the intrinsic name is '%' + global property name + '%'). - */ -export function getGlobalIntrinsics() { - const result = { __proto__: null }; - - for (const name of globalIntrinsicNames) { - const desc = getOwnPropertyDescriptor(globalThis, name); - if (desc) { - // Abort if an accessor is found on the unsafe global object - // instead of a data property. We should never get into this - // non standard situation. - if ('get' in desc || 'set' in desc) { - throw new TypeError(`Unexpected accessor on global property: ${name}`); - } - - result[name] = desc.value; - } - } - - return result; -} diff --git a/packages/ses/src/intrinsics.js b/packages/ses/src/intrinsics.js index 6fd277acf3..395da5d797 100644 --- a/packages/ses/src/intrinsics.js +++ b/packages/ses/src/intrinsics.js @@ -1,92 +1,152 @@ -// The intrinsics are the defiend in the global specifications. -// -// API -// -// getIntrinsics(): Object -// -// Operation similar to abstract operation `CreateInrinsics` in section 8.2.2 -// of the ES specifications. -// -// Return a record-like object similar to the [[intrinsics]] slot of the -// realmRec excepts for the following simpifications: -// -// - we omit the intrinsics not reachable by JavaScript code. -// -// - we omit intrinsics that are direct properties of the global object -// (except for the "prototype" property), and properties that are direct -// properties of the prototypes (except for "constructor"). -// -// - we use the name of the associated global object property instead of the -// intrinsic name (usually, ` === '%' + + '%'`). -// -// Assumptions -// -// The intrinsic names correspond to the object names with "%" added as prefix and suffix, i.e. the intrinsic "%Object%" is equal to the global object property "Object". +import { + defineProperty, + entries, + freeze, + getOwnPropertyDescriptor, + getOwnPropertyDescriptors, + objectHasOwnProperty, + values, +} from './commons.js'; -import { checkAnonIntrinsics } from './check-anon-intrinsics.js'; -import { getAnonymousIntrinsics } from './get-anonymous-intrinsics.js'; -import { intrinsicNames } from './intrinsic-names.js'; -import { getNamedIntrinsic } from './get-named-intrinsic.js'; -import { checkIntrinsics } from './check-intrinsics.js'; +import { + constantProperties, + sharedGlobalPropertyNames, + universalPropertyNames, + whitelist, +} from './whitelist.js'; -const { apply } = Reflect; -const uncurryThis = fn => (thisArg, ...args) => apply(fn, thisArg, args); -const hasOwnProperty = uncurryThis(Object.prototype.hasOwnProperty); +// Like defineProperty, but throws if it would modify an existing property. +// We use this to ensure that two conflicting attempts to define the same +// property throws, causing SES initialization to fail. Otherwise, a +// conflict between, for example, two of SES's internal whitelists might +// get masked as one overwrites the other. Accordingly, the thrown error +// complains of a "Conflicting definition". +function initProperty(obj, name, desc) { + if (objectHasOwnProperty(obj, name)) { + const preDesc = getOwnPropertyDescriptor(obj, name); + if ( + !Object.is(preDesc.value, desc.value) || + preDesc.get !== desc.get || + preDesc.set !== desc.set || + preDesc.writable !== desc.writable || + preDesc.enumerable !== desc.enumerable || + preDesc.configurable !== desc.configurable + ) { + throw new Error(`Conflicting definitions of ${name}`); + } + } + defineProperty(obj, name, desc); +} -const suffix = 'Prototype'; +// Like defineProperties, but throws if it would modify an existing property. +// This ensures that the intrinsics added to the intrinsics collector object +// graph do not overlap. +function initProperties(obj, descs) { + for (const [name, desc] of entries(descs)) { + initProperty(obj, name, desc); + } +} -/** - * getIntrinsics() - * Return a record-like object similar to the [[intrinsics]] slot of the realmRec - * excepts for the following simpifications: - * - we omit the intrinsics not reachable by JavaScript code. - * - we omit intrinsics that are direct properties of the global object (except for the - * "prototype" property), and properties that are direct properties of the prototypes - * (except for "constructor"). - * - we use the name of the associated global object property instead of the intrinsic - * name (usually, === '%' + + '%'). - */ -export function getIntrinsics() { - const intrinsics = { __proto__: null }; +// sampleGlobals creates an intrinsics object, suitable for +// interinsicsCollector.addIntrinsics, from the named properties of a global +// object. +function sampleGlobals(globalObject, newPropertyNames) { + const newIntrinsics = { __proto__: null }; + for (const [globalName, intrinsicName] of entries(newPropertyNames)) { + newIntrinsics[intrinsicName] = globalObject[globalName]; + } + return newIntrinsics; +} - const anonIntrinsics = getAnonymousIntrinsics(); - checkAnonIntrinsics(anonIntrinsics); +export function makeIntrinsicsCollector() { + const intrinsics = { __proto__: null }; + let pseudoNatives; - for (const name of intrinsicNames) { - if (hasOwnProperty(anonIntrinsics, name)) { - intrinsics[name] = anonIntrinsics[name]; - // eslint-disable-next-line no-continue - continue; - } + const intrinsicsCollector = { + addIntrinsics(newIntrinsics) { + initProperties(intrinsics, getOwnPropertyDescriptors(newIntrinsics)); + }, - if (hasOwnProperty(globalThis, name)) { - intrinsics[name] = getNamedIntrinsic(globalThis, name); - // eslint-disable-next-line no-continue - continue; - } + // For each intrinsic, if it has a `.prototype` property, use the + // whitelist to find out the intrinsic name for that prototype and add it + // to the intrinsics. + completePrototypes() { + for (const [name, intrinsic] of entries(intrinsics)) { + if (intrinsic !== Object(intrinsic)) { + // eslint-disable-next-line no-continue + continue; + } + if (!objectHasOwnProperty(intrinsic, 'prototype')) { + // eslint-disable-next-line no-continue + continue; + } + const permit = whitelist[name]; + if (typeof permit !== 'object') { + throw new Error(`Expected permit object at whitelist.${name}`); + } + const namePrototype = permit.prototype; + if (!namePrototype) { + throw new Error(`${name}.prototype property not whitelisted`); + } + if ( + typeof namePrototype !== 'string' || + !objectHasOwnProperty(whitelist, namePrototype) + ) { + throw new Error(`Unrecognized ${name}.prototype whitelist entry`); + } + const intrinsicPrototype = intrinsic.prototype; + if (objectHasOwnProperty(intrinsics, namePrototype)) { + if (intrinsics[namePrototype] !== intrinsicPrototype) { + throw new Error(`Conflicting bindings of ${namePrototype}`); + } + // eslint-disable-next-line no-continue + continue; + } + intrinsics[namePrototype] = intrinsicPrototype; + } + }, + finalIntrinsics() { + freeze(intrinsics); + pseudoNatives = new WeakSet( + values(intrinsics).filter(obj => typeof obj === 'function'), + ); + return intrinsics; + }, + isPseudoNative(obj) { + if (!pseudoNatives) { + throw new Error( + `isPseudoNative can only be called after finalIntrinsics`, + ); + } + return pseudoNatives.has(obj); + }, + }; - const hasSuffix = name.endsWith(suffix); - if (hasSuffix) { - const prefix = name.slice(0, -suffix.length); + intrinsicsCollector.addIntrinsics(constantProperties); + intrinsicsCollector.addIntrinsics( + sampleGlobals(globalThis, universalPropertyNames), + ); - if (hasOwnProperty(anonIntrinsics, prefix)) { - const intrinsic = anonIntrinsics[prefix]; - intrinsics[name] = intrinsic.prototype; - // eslint-disable-next-line no-continue - continue; - } + return intrinsicsCollector; +} - if (hasOwnProperty(globalThis, prefix)) { - const intrinsic = getNamedIntrinsic(globalThis, prefix); - intrinsics[name] = intrinsic.prototype; - // eslint-disable-next-line no-continue - continue; - } - } - } +/** + * getGlobalIntrinsics() + * Doesn't tame, delete, or modify anything. Samples globalObject to create an + * intrinsics record containing only the whitelisted global variables, listed + * by the intrinsic names appropriate for new globals, i.e., the globals of + * newly constructed compartments. + * + * WARNING: + * If run before lockdown, the returned intrinsics record will carry the + * *original* unsafe (feral, untamed) bindings of these global variables. + */ +export function getGlobalIntrinsics(globalObject) { + const intrinsicsCollector = makeIntrinsicsCollector(); - checkIntrinsics(intrinsics); + intrinsicsCollector.addIntrinsics( + sampleGlobals(globalObject, sharedGlobalPropertyNames), + ); - return intrinsics; + return intrinsicsCollector.finalIntrinsics(); } diff --git a/packages/ses/src/lockdown-shim.js b/packages/ses/src/lockdown-shim.js index ed4ff246cf..068b72bff7 100644 --- a/packages/ses/src/lockdown-shim.js +++ b/packages/ses/src/lockdown-shim.js @@ -15,18 +15,21 @@ import makeHardener from '@agoric/make-hardener'; import { assert } from './assert.js'; -import { defineProperties } from './commons.js'; -import { getIntrinsics } from './intrinsics.js'; +import { keys } from './commons.js'; +import { makeIntrinsicsCollector } from './intrinsics.js'; import whitelistIntrinsics from './whitelist-intrinsics.js'; import repairLegacyAccessors from './repair-legacy-accessors.js'; import tameFunctionConstructors from './tame-function-constructors.js'; -import tameGlobalDateObject from './tame-global-date-object.js'; -import tameGlobalErrorObject from './tame-global-error-object.js'; -import tameGlobalMathObject from './tame-global-math-object.js'; -import tameGlobalRegExpObject from './tame-global-reg-exp-object.js'; +import tameDateConstructor from './tame-date-constructor.js'; +import tameErrorConstructor from './tame-error-constructor.js'; +import tameMathObject from './tame-math-object.js'; +import tameRegExpConstructor from './tame-regexp-constructor.js'; import enablePropertyOverrides from './enable-property-overrides.js'; import tameLocaleMethods from './tame-locale-methods.js'; +import { getAnonymousIntrinsics } from './get-anonymous-intrinsics.js'; +import { initGlobalObject } from './global-object.js'; +import { initialGlobalPropertyNames } from './whitelist.js'; let firstOptions; @@ -43,7 +46,9 @@ export const harden = ref => { return lockdownHarden(ref); }; -export function lockdown(options = {}) { +const alreadyHardenedIntrinsics = () => false; + +export function repairIntrinsics(options = {}) { // First time, absent options default to 'safe'. // Subsequent times, absent options default to first options. // Thus, all present options must agree with first options. @@ -70,15 +75,13 @@ export function lockdown(options = {}) { // Asserts for multiple invocation of lockdown(). if (firstOptions) { - Object.keys(firstOptions).forEach(name => { + for (const name of keys(firstOptions)) { assert( options[name] === firstOptions[name], `lockdown(): cannot re-invoke with different option ${name}`, ); - }); - // Returning `false` indicates that lockdown() made no changes because it - // was invoked from SES with non-conflicting options. - return false; + } + return alreadyHardenedIntrinsics; } firstOptions = { @@ -90,52 +93,67 @@ export function lockdown(options = {}) { }; /** - * 1. TAME powers first. + * 1. TAME powers & gather intrinsics first. */ - tameFunctionConstructors(); + const intrinsicsCollector = makeIntrinsicsCollector(); - // TODO The tame functions return bindings for both start and shared - // which we should make use of in a principled manner. Until then, - // we just use the specific start binding explicitly. - defineProperties(globalThis, tameGlobalDateObject(dateTaming).start); - defineProperties(globalThis, tameGlobalErrorObject(errorTaming).start); - defineProperties(globalThis, tameGlobalMathObject(mathTaming).start); - defineProperties(globalThis, tameGlobalRegExpObject(regExpTaming).start); + intrinsicsCollector.addIntrinsics(tameFunctionConstructors()); - /** - * 2. WHITELIST to standardize the environment. - */ + intrinsicsCollector.addIntrinsics(tameDateConstructor(dateTaming)); + intrinsicsCollector.addIntrinsics(tameErrorConstructor(errorTaming)); + intrinsicsCollector.addIntrinsics(tameMathObject(mathTaming)); + intrinsicsCollector.addIntrinsics(tameRegExpConstructor(regExpTaming)); - // Extract the intrinsics from the global. - const intrinsics = getIntrinsics(); + intrinsicsCollector.addIntrinsics(getAnonymousIntrinsics()); + + intrinsicsCollector.completePrototypes(); + + const intrinsics = intrinsicsCollector.finalIntrinsics(); // Replace *Locale* methods with their non-locale equivalents tameLocaleMethods(intrinsics, localeTaming); + /** + * 2. WHITELIST to standardize the environment. + */ + // Remove non-standard properties. whitelistIntrinsics(intrinsics); // Repair problems with legacy accessors if necessary. repairLegacyAccessors(); + // Initialize the powerful initial global, i.e., the global of the + // start compartment, from the intrinsics. + initGlobalObject(globalThis, intrinsics, initialGlobalPropertyNames, {}); + /** * 3. HARDEN to share the intrinsics. */ - // Circumvent the override mistake. - const detachedProperties = enablePropertyOverrides(intrinsics); + function hardenIntrinsics() { + // Circumvent the override mistake. + const detachedProperties = enablePropertyOverrides(intrinsics); - // Finally register and optionally freeze all the intrinsics. This - // must be the operation that modifies the intrinsics. - lockdownHarden(intrinsics); - lockdownHarden(detachedProperties); + // Finally register and optionally freeze all the intrinsics. This + // must be the operation that modifies the intrinsics. + lockdownHarden(intrinsics); + lockdownHarden(detachedProperties); - // Having completed lockdown without failing, the user may now - // call `harden` and expect the object's transitively accessible properties - // to be frozen out to the fringe. - // Raise the `harden` gate. - lockedDown = true; + // Having completed lockdown without failing, the user may now + // call `harden` and expect the object's transitively accessible properties + // to be frozen out to the fringe. + // Raise the `harden` gate. + lockedDown = true; - // Returning `true` indicates that this is a JS to SES transition. - return true; + // Returning `true` indicates that this is a JS to SES transition. + return true; + } + + return hardenIntrinsics; } + +export const lockdown = (options = {}) => { + const maybeHardenIntrinsics = repairIntrinsics(options); + return maybeHardenIntrinsics(); +}; diff --git a/packages/ses/src/make-eval-function.js b/packages/ses/src/make-eval-function.js index 5fac000a36..5abd2bc6d0 100644 --- a/packages/ses/src/make-eval-function.js +++ b/packages/ses/src/make-eval-function.js @@ -6,19 +6,21 @@ import { performEval } from './evaluate.js'; * A safe version of the native eval function which relies on * the safety of performEval for confinement. */ -export const makeEvalFunction = (realmRec, globalObject, options = {}) => { +export const makeEvalFunction = (globalObject, options = {}) => { // We use the the concise method syntax to create an eval without a // [[Construct]] behavior (such that the invocation "new eval()" throws // TypeError: eval is not a constructor"), but which still accepts a // 'this' binding. const newEval = { - eval(x) { - if (typeof x !== 'string') { + eval(source) { + if (typeof source !== 'string') { // As per the runtime semantic of PerformEval [ECMAScript 18.2.1.1]: - // If Type(x) is not String, return x. - return x; + // If Type(source) is not String, return source. + // TODO Recent proposals from Mike Samuel may change this non-string + // rule. Track. + return source; } - return performEval(realmRec, x, globalObject, {}, options); + return performEval(source, globalObject, {}, options); }, }.eval; diff --git a/packages/ses/src/make-evaluate-factory.js b/packages/ses/src/make-evaluate-factory.js index d6fec8168d..84906266a0 100644 --- a/packages/ses/src/make-evaluate-factory.js +++ b/packages/ses/src/make-evaluate-factory.js @@ -1,5 +1,10 @@ import { arrayJoin } from './commons.js'; +// The original unsafe untamed Function constructor, which must not escape. +// Sample at module initialization time, which is before lockdown can +// repair it. Use it only to build powerless abstractions. +const FERAL_FUNCTION = Function; + /** * buildOptimizer() * Given an array of indentifier, the optimizer return a `const` declaration @@ -18,7 +23,7 @@ function buildOptimizer(constants) { * The factory create 'evaluate' functions with the correct optimizer * inserted. */ -export function makeEvaluateFactory(realmRec, constants = []) { +export function makeEvaluateFactory(constants = []) { const optimizer = buildOptimizer(constants); // Create a function in sloppy mode, so that we can use 'with'. It returns @@ -46,7 +51,7 @@ export function makeEvaluateFactory(realmRec, constants = []) { // eval intrinsic, and flips useUnsafeEvaluator back to false. Any reference // to 'eval' in that string will get the tamed evaluator. - return realmRec.intrinsics.Function(` + return FERAL_FUNCTION(` with (this) { ${optimizer} return function() { diff --git a/packages/ses/src/make-function-constructor.js b/packages/ses/src/make-function-constructor.js index f0dc3e5daf..7ce798fe1d 100644 --- a/packages/ses/src/make-function-constructor.js +++ b/packages/ses/src/make-function-constructor.js @@ -7,12 +7,17 @@ import { } from './commons.js'; import { performEval } from './evaluate.js'; +// The original unsafe untamed Function constructor, which must not escape. +// Sample at module initialization time, which is before lockdown can +// repair it. Use it only to build powerless abstractions. +const FERAL_FUNCTION = Function; + /** * makeFunctionConstructor() * A safe version of the native Function which relies on * the safety of performEval for confinement. */ -export function makeFunctionConstructor(realmRec, globaObject, options = {}) { +export function makeFunctionConstructor(globaObject, options = {}) { // Define an unused parameter to ensure Function.length === 1 // eslint-disable-next-line no-unused-vars const newFunction = function Function(body) { @@ -28,20 +33,20 @@ export function makeFunctionConstructor(realmRec, globaObject, options = {}) { // - bodyText doesn't parse as a function body // - either contain a call to super() or references a super property. // eslint-disable-next-line no-new - new realmRec.intrinsics.Function(parameters, bodyText); + new FERAL_FUNCTION(parameters, bodyText); // Safe to be combined. Defeat potential trailing comments. // TODO: since we create an anonymous function, the 'this' value // isn't bound to the global object as per specs, but set as undefined. const src = `(function anonymous(${parameters}\n) {\n${bodyText}\n})`; - return performEval(realmRec, src, globaObject, {}, options); + return performEval(src, globaObject, {}, options); }; defineProperties(newFunction, { // Ensure that any function created in any evaluator in a realm is an // instance of Function in any evaluator of the same realm. prototype: { - value: realmRec.intrinsics.Function.prototype, + value: Function.prototype, writable: false, enumerable: false, configurable: false, diff --git a/packages/ses/src/module-instance.js b/packages/ses/src/module-instance.js index cf61379ece..4ff0b34ed4 100644 --- a/packages/ses/src/module-instance.js +++ b/packages/ses/src/module-instance.js @@ -1,5 +1,4 @@ import { performEval } from './evaluate.js'; -import { getCurrentRealmRec } from './realm-rec.js'; import { getDeferredExports } from './module-proxy.js'; import { create, @@ -313,9 +312,7 @@ export const makeModuleInstance = ( activate(); } - const realmRec = getCurrentRealmRec(); let optFunctor = performEval( - realmRec, functorSource, globalObject, localObject, // live bindings over global lexicals diff --git a/packages/ses/src/realm-rec.js b/packages/ses/src/realm-rec.js deleted file mode 100644 index db748b106a..0000000000 --- a/packages/ses/src/realm-rec.js +++ /dev/null @@ -1,32 +0,0 @@ -import { getGlobalIntrinsics } from './intrinsics-global.js'; -import { freeze } from './commons.js'; - -// Note: Instead of using a safe*/unsafe* naming convention as a label to -// indentify sources of power, we simply use realmRec as the powerful object, -// and we always reference properties directly on it, which has the benefit -// of decreasing the number of moving parts. - -let realmRec; - -/** - * getCurrentRealmRec() - * Creates a realm-like record, minus what we don't need or can't emulate. - * The realm record (ECMAScript 8.2) holds the intrinsics, the global - * object, the global environment, etc. - */ -export function getCurrentRealmRec() { - if (realmRec) { - return realmRec; - } - - // We don't freeze the intrinsics record itself so it can be customized. - const intrinsics = getGlobalIntrinsics(); - - realmRec = { - __proto__: null, - intrinsics, - }; - - // However, we freeze the realm record for safety. - return freeze(realmRec); -} diff --git a/packages/ses/src/scope-handler.js b/packages/ses/src/scope-handler.js index f2937627d4..a364fb35e7 100644 --- a/packages/ses/src/scope-handler.js +++ b/packages/ses/src/scope-handler.js @@ -6,6 +6,12 @@ import { reflectSet, } from './commons.js'; +// The original unsafe untamed eval function, which must not escape. +// Sample at module initialization time, which is before lockdown can +// repair it. Use it only to build powerless abstractions. +// eslint-disable-next-line no-eval +const FERAL_EVAL = eval; + /** * alwaysThrowHandler * This is an object that throws if any propery is called. It's used as @@ -14,7 +20,7 @@ import { * create one and share it between all scopeHandlers. */ const alwaysThrowHandler = new Proxy(immutableObject, { - get(shadow, prop) { + get(_shadow, prop) { throwTantrum(`unexpected scope handler trap called: ${String(prop)}`); }, }); @@ -35,7 +41,6 @@ const alwaysThrowHandler = new Proxy(immutableObject, { * - ensure the Proxy invariants despite some global properties being frozen. */ export function createScopeHandler( - realmRec, globalObject, localObject = {}, { sloppyGlobalsMode = false } = {}, @@ -49,7 +54,7 @@ export function createScopeHandler( // realm's code or if it is user-land invocation, so we can react differently. useUnsafeEvaluator: false, - get(shadow, prop) { + get(_shadow, prop) { if (typeof prop === 'symbol') { return undefined; } @@ -62,7 +67,7 @@ export function createScopeHandler( if (this.useUnsafeEvaluator === true) { // revoke before use this.useUnsafeEvaluator = false; - return realmRec.intrinsics.eval; + return FERAL_EVAL; } // fall through } @@ -88,7 +93,7 @@ export function createScopeHandler( return reflectGet(globalObject, prop); }, - set(shadow, prop, value) { + set(_shadow, prop, value) { // Properties of the localObject. if (prop in localObject) { const desc = getOwnPropertyDescriptor(localObject, prop); @@ -124,7 +129,7 @@ export function createScopeHandler( // accept assignments to undefined globals, when it ought to throw // ReferenceError for such assignments) - has(shadow, prop) { + has(_shadow, prop) { // unsafeGlobal: hide all properties of the current global // at the expense of 'typeof' being wrong for those properties. For // example, in the browser, evaluating 'document = 3', will add diff --git a/packages/ses/src/tame-date-constructor.js b/packages/ses/src/tame-date-constructor.js new file mode 100644 index 0000000000..f358b8ac48 --- /dev/null +++ b/packages/ses/src/tame-date-constructor.js @@ -0,0 +1,104 @@ +import { defineProperties } from './commons.js'; + +export default function tameDateConstructor(dateTaming = 'safe') { + if (dateTaming !== 'safe' && dateTaming !== 'unsafe') { + throw new Error(`unrecognized dateTaming ${dateTaming}`); + } + const OriginalDate = Date; + const DatePrototype = OriginalDate.prototype; + + // Use concise methods to obtain named functions without constructors. + const tamedMethods = { + now() { + return NaN; + }, + }; + + // Tame the Date constructor. + // Common behavior + // * new Date(x) coerces x into a number and then returns a Date + // for that number of millis since the epoch + // * new Date(NaN) returns a Date object which stringifies to + // 'Invalid Date' + // * new Date(undefined) returns a Date object which stringifies to + // 'Invalid Date' + // OriginalDate (normal standard) behavior + // * Date(anything) gives a string with the current time + // * new Date() returns the current time, as a Date object + // SharedDate behavior + // * Date(anything) returned 'Invalid Date' + // * new Date() returns a Date object which stringifies to + // 'Invalid Date' + const makeDateConstructor = ({ powers = 'none' } = {}) => { + let ResultDate; + if (powers === 'original') { + ResultDate = function Date(...rest) { + if (new.target === undefined) { + return Reflect.apply(OriginalDate, undefined, rest); + } + return Reflect.construct(OriginalDate, rest, new.target); + }; + } else { + ResultDate = function Date(...rest) { + if (new.target === undefined) { + return 'Invalid Date'; + } + if (rest.length === 0) { + rest = [NaN]; + } + return Reflect.construct(OriginalDate, rest, new.target); + }; + } + + defineProperties(ResultDate, { + length: { value: 7 }, + prototype: { + value: DatePrototype, + writable: false, + enumerable: false, + configurable: false, + }, + parse: { + value: Date.parse, + writable: true, + enumerable: false, + configurable: true, + }, + UTC: { + value: Date.UTC, + writable: true, + enumerable: false, + configurable: true, + }, + }); + return ResultDate; + }; + const InitialDate = makeDateConstructor({ powers: 'original' }); + const SharedDate = makeDateConstructor({ power: 'none' }); + + defineProperties(InitialDate, { + now: { + value: Date.now, + writable: true, + enumerable: false, + configurable: true, + }, + }); + defineProperties(SharedDate, { + now: { + value: tamedMethods.now, + writable: true, + enumerable: false, + configurable: true, + }, + }); + + defineProperties(DatePrototype, { + constructor: { value: SharedDate }, + }); + + return { + '%InitialDate%': InitialDate, + '%SharedDate%': SharedDate, + }; +} diff --git a/packages/ses/src/tame-error-constructor.js b/packages/ses/src/tame-error-constructor.js new file mode 100644 index 0000000000..ce6fd35ad0 --- /dev/null +++ b/packages/ses/src/tame-error-constructor.js @@ -0,0 +1,80 @@ +import { defineProperties, setPrototypeOf } from './commons.js'; +import { tameV8ErrorConstructor } from './tame-v8-error-constructor.js'; + +// TODO where should this go? +export const NativeErrors = [ + EvalError, + RangeError, + ReferenceError, + SyntaxError, + TypeError, + URIError, +]; + +// Use concise methods to obtain named functions without constructors. +const tamedMethods = { + getStackString(_error) { + return ''; + }, +}; + +export default function tameErrorConstructor(errorTaming = 'safe') { + if (errorTaming !== 'safe' && errorTaming !== 'unsafe') { + throw new Error(`unrecognized errorTaming ${errorTaming}`); + } + const OriginalError = Error; + const ErrorPrototype = OriginalError.prototype; + + const makeErrorConstructor = (_ = {}) => { + const ResultError = function Error(...rest) { + if (new.target === undefined) { + return OriginalError(...rest); + } + return Reflect.construct(OriginalError, rest, new.target); + }; + defineProperties(ResultError, { + length: { value: 1 }, + prototype: { + value: ErrorPrototype, + writable: false, + enumerable: false, + configurable: false, + }, + }); + return ResultError; + }; + const InitialError = makeErrorConstructor({ powers: 'original' }); + const SharedError = makeErrorConstructor({ powers: 'none' }); + defineProperties(ErrorPrototype, { + constructor: { value: SharedError }, + /* TODO + stack: { + get() { + return ''; + }, + set(_) { + // ignore + }, + }, + */ + }); + + for (const NativeError of NativeErrors) { + setPrototypeOf(NativeError, SharedError); + } + + let initialGetStackString = tamedMethods.getStackString; + if (typeof OriginalError.captureStackTrace === 'function') { + // Assume we're on v8 + initialGetStackString = tameV8ErrorConstructor( + OriginalError, + InitialError, + errorTaming, + ); + } + return { + '%InitialGetStackString%': initialGetStackString, + '%InitialError%': InitialError, + '%SharedError%': SharedError, + }; +} diff --git a/packages/ses/src/tame-function-constructors.js b/packages/ses/src/tame-function-constructors.js index c6e0ab60b7..a274aa93b1 100644 --- a/packages/ses/src/tame-function-constructors.js +++ b/packages/ses/src/tame-function-constructors.js @@ -35,16 +35,17 @@ import { defineProperties, getPrototypeOf, setPrototypeOf } from './commons.js'; * %GeneratorFunction% %AsyncFunction% and %AsyncGeneratorFunction%, with * safe replacements that throw if invoked. */ - export default function tameFunctionConstructors() { try { // Verify that the method is not callable. (0, Function.prototype.constructor)('return 1'); } catch (ignore) { // Throws, no need to patch. - return; + return {}; } + const newIntrinsics = {}; + /** * The process to repair constructors: * 1. Create an instance of the function by evaluating syntax @@ -54,7 +55,7 @@ export default function tameFunctionConstructors() { * 5. Replace tamed constructor prototype property with the original one * 6. Replace its [[Prototype]] slot with the tamed constructor of Function */ - function repairFunction(name, declaration) { + function repairFunction(name, intrinsicName, declaration) { let FunctionInstance; try { // eslint-disable-next-line no-eval @@ -73,10 +74,11 @@ export default function tameFunctionConstructors() { // Prevents the evaluation of source when calling constructor on the // prototype of functions. // eslint-disable-next-line func-names - const constructor = function() { + const InertConstructor = function() { throw new TypeError('Not available'); }; - defineProperties(constructor, { + defineProperties(InertConstructor, { + prototype: { value: FunctionPrototype }, name: { value: name, writable: false, @@ -92,28 +94,38 @@ export default function tameFunctionConstructors() { }); defineProperties(FunctionPrototype, { - constructor: { value: constructor }, - }); - - // This line sets the tamed constructor's prototype data property to - // the original one. - defineProperties(constructor, { - prototype: { value: FunctionPrototype }, + constructor: { value: InertConstructor }, }); // Reconstructs the inheritance among the new tamed constructors // to mirror the original specified in normal JS. - if (constructor !== Function.prototype.constructor) { - setPrototypeOf(constructor, Function.prototype.constructor); + if (InertConstructor !== Function.prototype.constructor) { + setPrototypeOf(InertConstructor, Function.prototype.constructor); } + + newIntrinsics[intrinsicName] = InertConstructor; } // Here, the order of operation is important: Function needs to be repaired // first since the other repaired constructors need to inherit from the // tamed Function function constructor. - repairFunction('Function', '(function(){})'); - repairFunction('GeneratorFunction', '(function*(){})'); - repairFunction('AsyncFunction', '(async function(){})'); - repairFunction('AsyncGeneratorFunction', '(async function*(){})'); + repairFunction('Function', '%InertFunction%', '(function(){})'); + repairFunction( + 'GeneratorFunction', + '%InertGeneratorFunction%', + '(function*(){})', + ); + repairFunction( + 'AsyncFunction', + '%InertAsyncFunction%', + '(async function(){})', + ); + repairFunction( + 'AsyncGeneratorFunction', + '%InertAsyncGeneratorFunction%', + '(async function*(){})', + ); + + return newIntrinsics; } diff --git a/packages/ses/src/tame-global-date-object.js b/packages/ses/src/tame-global-date-object.js deleted file mode 100644 index 9aa073319f..0000000000 --- a/packages/ses/src/tame-global-date-object.js +++ /dev/null @@ -1,92 +0,0 @@ -import { defineProperties } from './commons.js'; - -export default function tameGlobalDateObject(dateTaming = 'safe') { - if (dateTaming !== 'safe' && dateTaming !== 'unsafe') { - throw new Error(`unrecognized dateTaming ${dateTaming}`); - } - const originalDate = Date; - - // Tame the Date constructor. - // Common behavior - // * new Date(x) coerces x into a number and then returns a Date - // for that number of millis since the epoch - // * new Date(NaN) returns a Date object which stringifies to - // 'Invalid Date' - // * new Date(undefined) returns a Date object which stringifies to - // 'Invalid Date' - // originalDate (normal standard) behavior - // * Date(anything) gives a string with the current time - // * new Date() returns the current time, as a Date object - // sharedDate behavior - // * Date(anything) returned 'Invalid Date' - // * new Date() returns a Date object which stringifies to - // 'Invalid Date' - const sharedDate = function Date(...rest) { - if (new.target === undefined) { - return 'Invalid Date'; - } - if (rest.length === 0) { - rest = [NaN]; - } - // todo: test that our constructor can still be subclassed - return Reflect.construct(originalDate, rest, new.target); - }; - - // Use concise methods to obtain named functions without constructors. - const tamedMethods = { - now() { - return NaN; - }, - }; - - const DatePrototype = originalDate.prototype; - defineProperties(sharedDate, { - length: { value: 7 }, - prototype: { - value: DatePrototype, - writable: false, - enumerable: false, - configurable: false, - }, - now: { - value: tamedMethods.now, - writable: true, - enumerable: false, - configurable: true, - }, - parse: { - value: Date.parse, - writable: true, - enumerable: false, - configurable: true, - }, - UTC: { - value: Date.UTC, - writable: true, - enumerable: false, - configurable: true, - }, - }); - defineProperties(DatePrototype, { - constructor: { value: sharedDate }, - }); - - return { - start: { - Date: { - value: dateTaming === 'unsafe' ? originalDate : sharedDate, - writable: true, - enumerable: false, - configurable: true, - }, - }, - shared: { - Date: { - value: sharedDate, - writable: true, - enumerable: false, - configurable: true, - }, - }, - }; -} diff --git a/packages/ses/src/tame-global-error-object.js b/packages/ses/src/tame-global-error-object.js deleted file mode 100644 index 1daa772e70..0000000000 --- a/packages/ses/src/tame-global-error-object.js +++ /dev/null @@ -1,293 +0,0 @@ -import { defineProperties, setPrototypeOf } from './commons.js'; - -// TODO where should this go? -export const NativeErrors = [ - EvalError, - RangeError, - ReferenceError, - SyntaxError, - TypeError, - URIError, -]; - -// Whitelist names from https://v8.dev/docs/stack-trace-api -// Whitelisting only the names used by error-stack-shim/src/v8StackFrames -// callSiteToFrame to shim the error stack proposal. -const safeV8CallSiteMethodNames = [ - // suppress 'getThis' definitely - 'getTypeName', - // suppress 'getFunction' definitely - 'getFunctionName', - 'getMethodName', - 'getFileName', - 'getLineNumber', - 'getColumnNumber', - 'getEvalOrigin', - 'isToplevel', - 'isEval', - 'isNative', - 'isConstructor', - 'isAsync', - // suppress 'isPromiseAll' for now - // suppress 'getPromiseIndex' for now - - // Additional names found by experiment, absent from - // https://v8.dev/docs/stack-trace-api - 'getPosition', - 'getScriptNameOrSourceURL', - - 'toString', // TODO replace to use only whitelisted info -]; - -// TODO this is a ridiculously expensive way to attenuate callsites. -// Before that matters, we should switch to a reasonable representation. -const safeV8CallSiteFacet = callSite => { - const methodEntry = name => [name, () => callSite[name]()]; - const o = Object.fromEntries(safeV8CallSiteMethodNames.map(methodEntry)); - return Object.create(o, {}); -}; - -const safeV8SST = sst => sst.map(safeV8CallSiteFacet); - -export default function tameGlobalErrorObject(errorTaming = 'safe') { - if (errorTaming !== 'safe' && errorTaming !== 'unsafe') { - throw new Error(`unrecognized errorTaming ${errorTaming}`); - } - const originalError = Error; - - // We never expose the originalError constructor. Rather, tamedError - // is the one to be installed on the start compartment. - const tamedError = function Error(...rest) { - if (new.target === undefined) { - return originalError(...rest); - } - return Reflect.construct(originalError, rest, new.target); - }; - - // TODO uncomment the sharedError occurrences. To do this, we need - // more intrinsic reform so that the whitelist doesn't get confused - // between tamedError and sharedError. - const sharedError = tamedError; - /* - const sharedError = function Error(...rest) { - if (new.target === undefined) { - return originalError(...rest); - } - return Reflect.construct(originalError, rest, new.target); - }; - */ - - // Use concise methods to obtain named functions without constructors. - const tamedMethods = { - // The optional `optFn` argument is for cutting off the bottom of - // the stack --- for capturing the stack only above the topmost - // call to that function. Since this isn't the "real" captureStackTrace - // but instead calls the real one, if no other cutoff is provided, - // we cut this one off. - captureStackTrace(error, optFn = tamedMethods.captureStackTrace) { - if ( - errorTaming === 'unsafe' && - typeof originalError.captureStackTrace === 'function' - ) { - // originalError.captureStackTrace is only on v8 - originalError.captureStackTrace(error, optFn); - return; - } - Reflect.set(error, 'stack', ''); - }, - }; - - // A prepareFn is a prepareStackTrace function. - // An sst is a `structuredStackTrace`, which is an array of - // callsites. - // A user prepareFn is a prepareFn defined by a client of this API, - // and provided by assigning to `Error.prepareStackTrace`. - // A user prepareFn should only receive an attenuated sst, which - // is an array of attenuated callsites. - // A system prepareFn is the prepareFn created by this module to - // be installed on the real `Error` constructor, to receive - // an original sst, i.e., an array of unattenuated callsites. - // An input prepareFn is a function the user assigns to - // `Error.prepareStackTrace`, which might be a user prepareFn or - // a system prepareFn previously obtained by reading - // `Error.prepareStackTrace`. - - // A weakset branding some functions as system prepareFns, all of which - // must be defined by this module, since they can receive an - // unattenuated sst. - const systemPrepareFnSet = new WeakSet(); - - const systemPrepareFnFor = inputPrepareFn => { - if (systemPrepareFnSet.has(inputPrepareFn)) { - return inputPrepareFn; - } - // Use concise methods to obtain named functions without constructors. - const systemMethods = { - prepareStackTrace(error, sst) { - return inputPrepareFn(error, safeV8SST(sst)); - }, - }; - systemPrepareFnSet.add(systemMethods.prepareStackTrace); - return systemMethods.prepareStackTrace; - }; - - const ErrorPrototype = originalError.prototype; - if (typeof originalError.captureStackTrace === 'function') { - // Define captureStackTrace only on v8 - defineProperties(tamedError, { - captureStackTrace: { - value: tamedMethods.captureStackTrace, - writable: true, - enumerable: false, - configurable: true, - }, - }); - } - defineProperties(tamedError, { - length: { value: 1 }, - prototype: { - value: ErrorPrototype, - writable: false, - enumerable: false, - configurable: false, - }, - stackTraceLimit: { - get() { - if ( - errorTaming === 'unsafe' && - typeof originalError.stackTraceLimit === 'number' - ) { - // originalError.stackTraceLimit is only on v8 - return originalError.stackTraceLimit; - } - return undefined; - }, - // https://v8.dev/docs/stack-trace-api#compatibility advises that - // programmers can "always" set `Error.stackTraceLimit` and - // `Error.prepareStackTrace` even on non-v8 platforms. On non-v8 - // it will have no effect, but this advise only makes sense - // if the assignment itself does not fail, which it would - // if `Error` were naively frozen. Hence, we add setters that - // accept but ignore the assignment on non-v8 platforms. - set(newLimit) { - if ( - errorTaming === 'unsafe' && - typeof originalError.stackTraceLimit === 'number' - ) { - // originalError.stackTraceLimit is only on v8 - originalError.stackTraceLimit = newLimit; - // We place the useless return on the next line to ensure - // that anything we place after the if in the future only - // happens if the then-case does not. - // eslint-disable-next-line no-useless-return - return; - } - }, - // WTF on v8 stackTraceLimit is enumerable - enumerable: false, - configurable: true, - }, - prepareStackTrace: { - get() { - if (errorTaming === 'unsafe') { - return originalError.prepareStackTrace; - } - // By returning undefined, hopefully this means the VM will next consult - // originalError.prepareStackTrace, even on node despite - // https://bugs.chromium.org/p/v8/issues/detail?id=10551#c3 - // or, if absent, fallback to the default behavior. - return undefined; - }, - set(inputPrepareStackTraceFn) { - if (errorTaming === 'unsafe') { - if (typeof inputPrepareStackTraceFn === 'function') { - const systemPrepareFn = systemPrepareFnFor( - inputPrepareStackTraceFn, - ); - originalError.prepareStackTrace = systemPrepareFn; - } else { - delete originalError.prepareStackTrace; - } - // We place the useless return on the next line to ensure - // that anything we place after the if in the future only - // happens if the then-case does not. - // eslint-disable-next-line no-useless-return - return; - } - }, - enumerable: false, - configurable: true, - }, - }); - - // TODO uncomment. See TODO note above - /* - defineProperties(sharedError, { - length: { value: 1 }, - prototype: { - value: ErrorPrototype, - writable: false, - enumerable: false, - configurable: false, - }, - stackTraceLimit: { - get() { - return undefined; - }, - set(_) { - // ignore - }, - // WTF on v8 stackTraceLimit is enumerable - enumerable: false, - configurable: true, - }, - prepareStackTrace: { - get() { - return undefined; - }, - set(_) { - // ignore - }, - enumerable: false, - configurable: true, - }, - }); - */ - - defineProperties(ErrorPrototype, { - constructor: { value: sharedError }, - /* TODO - stack: { - get() { - return ''; - }, - set(_) { - // ignore - }, - }, - */ - }); - - for (const NativeError of NativeErrors) { - setPrototypeOf(NativeError, sharedError); - } - - return { - start: { - Error: { - value: tamedError, - writable: true, - enumerable: false, - configurable: true, - }, - }, - shared: { - Error: { - value: sharedError, - writable: true, - enumerable: false, - configurable: true, - }, - }, - }; -} diff --git a/packages/ses/src/tame-global-math-object.js b/packages/ses/src/tame-global-math-object.js deleted file mode 100644 index 1f723f25f4..0000000000 --- a/packages/ses/src/tame-global-math-object.js +++ /dev/null @@ -1,46 +0,0 @@ -import { create, getOwnPropertyDescriptors } from './commons.js'; - -export default function tameGlobalMathObject(mathTaming = 'safe') { - if (mathTaming !== 'safe' && mathTaming !== 'unsafe') { - throw new Error(`unrecognized mathTaming ${mathTaming}`); - } - const originalMath = Math; - - // Tame the %Math% intrinsic. - - // Use concise methods to obtain named functions without constructors. - const tamedMethods = { - random() { - throw TypeError('Math.random() is disabled'); - }, - }; - - const sharedMath = create(Object.prototype, { - ...getOwnPropertyDescriptors(originalMath), - random: { - value: tamedMethods.random, - writable: true, - enumerable: false, - configurable: true, - }, - }); - - return { - start: { - Math: { - value: mathTaming === 'unsafe' ? originalMath : sharedMath, - writable: true, - enumerable: false, - configurable: true, - }, - }, - shared: { - Math: { - value: sharedMath, - writable: true, - enumerable: false, - configurable: true, - }, - }, - }; -} diff --git a/packages/ses/src/tame-global-reg-exp-object.js b/packages/ses/src/tame-global-reg-exp-object.js deleted file mode 100644 index 08e71558a1..0000000000 --- a/packages/ses/src/tame-global-reg-exp-object.js +++ /dev/null @@ -1,54 +0,0 @@ -import { defineProperties, getOwnPropertyDescriptor } from './commons.js'; - -export default function tameGlobalRegExpObject(regExpTaming = 'safe') { - if (regExpTaming !== 'safe' && regExpTaming !== 'unsafe') { - throw new Error(`unrecognized regExpTaming ${regExpTaming}`); - } - const originalRegExp = RegExp; - - // RegExp has non-writable static properties we need to omit. - const sharedRegExp = function RegExp(...rest) { - if (new.target === undefined) { - return originalRegExp(...rest); - } - return Reflect.construct(originalRegExp, rest, new.target); - }; - - const RegExpPrototype = originalRegExp.prototype; - defineProperties(sharedRegExp, { - length: { value: 2 }, - prototype: { - value: RegExpPrototype, - writable: false, - enumerable: false, - configurable: false, - }, - [Symbol.species]: getOwnPropertyDescriptor(originalRegExp, Symbol.species), - }); - - delete RegExpPrototype.compile; - defineProperties(RegExpPrototype, { - constructor: { value: sharedRegExp }, - }); - - return { - start: { - // TODO do we ever really want to expose the original RegExp constructor, - // even just in the start compartment? - RegExp: { - value: regExpTaming === 'unsafe' ? originalRegExp : sharedRegExp, - writable: true, - enumerable: false, - configurable: true, - }, - }, - shared: { - RegExp: { - value: sharedRegExp, - writable: true, - enumerable: false, - configurable: true, - }, - }, - }; -} diff --git a/packages/ses/src/tame-locale-methods.js b/packages/ses/src/tame-locale-methods.js index ffe3280fca..c8904d895b 100644 --- a/packages/ses/src/tame-locale-methods.js +++ b/packages/ses/src/tame-locale-methods.js @@ -42,20 +42,22 @@ export default function tameLocaleMethods(intrinsics, localeTaming = 'safe') { for (const intrinsicName of getOwnPropertyNames(intrinsics)) { const intrinsic = intrinsics[intrinsicName]; - for (const methodName of getOwnPropertyNames(intrinsic)) { - const match = localePattern.exec(methodName); - if (match) { - assert( - typeof intrinsic[methodName] === 'function', - `expected ${methodName} to be a function`, - ); - const nonLocaleMethodName = `${match[1]}${match[2]}`; - const method = intrinsic[nonLocaleMethodName]; - assert( - typeof method === 'function', - `function ${nonLocaleMethodName} not found`, - ); - defineProperty(intrinsic, methodName, { value: method }); + if (intrinsic === Object(intrinsic)) { + for (const methodName of getOwnPropertyNames(intrinsic)) { + const match = localePattern.exec(methodName); + if (match) { + assert( + typeof intrinsic[methodName] === 'function', + `expected ${methodName} to be a function`, + ); + const nonLocaleMethodName = `${match[1]}${match[2]}`; + const method = intrinsic[nonLocaleMethodName]; + assert( + typeof method === 'function', + `function ${nonLocaleMethodName} not found`, + ); + defineProperty(intrinsic, methodName, { value: method }); + } } } } diff --git a/packages/ses/src/tame-math-object.js b/packages/ses/src/tame-math-object.js new file mode 100644 index 0000000000..2e620e920e --- /dev/null +++ b/packages/ses/src/tame-math-object.js @@ -0,0 +1,23 @@ +import { create, getOwnPropertyDescriptors } from './commons.js'; + +export default function tameMathObject(mathTaming = 'safe') { + if (mathTaming !== 'safe' && mathTaming !== 'unsafe') { + throw new Error(`unrecognized mathTaming ${mathTaming}`); + } + const originalMath = Math; + const initialMath = originalMath; // to follow the naming pattern + + // TODO I shouldn't need this eslint-disable for an ignored _ or variable + // name beginning with underbar. + // eslint-disable-next-line no-unused-vars + const { random: _, ...otherDescriptors } = getOwnPropertyDescriptors( + originalMath, + ); + + const sharedMath = create(Object.prototype, otherDescriptors); + + return { + '%InitialMath%': initialMath, + '%SharedMath%': sharedMath, + }; +} diff --git a/packages/ses/src/tame-regexp-constructor.js b/packages/ses/src/tame-regexp-constructor.js new file mode 100644 index 0000000000..bed644f52a --- /dev/null +++ b/packages/ses/src/tame-regexp-constructor.js @@ -0,0 +1,49 @@ +import { defineProperties, getOwnPropertyDescriptor } from './commons.js'; + +export default function tameRegExpConstructor(regExpTaming = 'safe') { + if (regExpTaming !== 'safe' && regExpTaming !== 'unsafe') { + throw new Error(`unrecognized regExpTaming ${regExpTaming}`); + } + const OriginalRegExp = RegExp; + const RegExpPrototype = OriginalRegExp.prototype; + + const makeRegExpConstructor = (_ = {}) => { + // RegExp has non-writable static properties we need to omit. + const ResultRegExp = function RegExp(...rest) { + if (new.target === undefined) { + return OriginalRegExp(...rest); + } + return Reflect.construct(OriginalRegExp, rest, new.target); + }; + + defineProperties(ResultRegExp, { + length: { value: 2 }, + prototype: { + value: RegExpPrototype, + writable: false, + enumerable: false, + configurable: false, + }, + [Symbol.species]: getOwnPropertyDescriptor( + OriginalRegExp, + Symbol.species, + ), + }); + return ResultRegExp; + }; + + const InitialRegExp = makeRegExpConstructor(); + const SharedRegExp = makeRegExpConstructor(); + + if (regExpTaming !== 'unsafe') { + delete RegExpPrototype.compile; + } + defineProperties(RegExpPrototype, { + constructor: { value: SharedRegExp }, + }); + + return { + '%InitialRegExp%': InitialRegExp, + '%SharedRegExp%': SharedRegExp, + }; +} diff --git a/packages/ses/src/tame-v8-error-constructor.js b/packages/ses/src/tame-v8-error-constructor.js new file mode 100644 index 0000000000..a417a927da --- /dev/null +++ b/packages/ses/src/tame-v8-error-constructor.js @@ -0,0 +1,187 @@ +import { defineProperties } from './commons.js'; + +// Whitelist names from https://v8.dev/docs/stack-trace-api +// Whitelisting only the names used by error-stack-shim/src/v8StackFrames +// callSiteToFrame to shim the error stack proposal. +const safeV8CallSiteMethodNames = [ + // suppress 'getThis' definitely + 'getTypeName', + // suppress 'getFunction' definitely + 'getFunctionName', + 'getMethodName', + 'getFileName', + 'getLineNumber', + 'getColumnNumber', + 'getEvalOrigin', + 'isToplevel', + 'isEval', + 'isNative', + 'isConstructor', + 'isAsync', + // suppress 'isPromiseAll' for now + // suppress 'getPromiseIndex' for now + + // Additional names found by experiment, absent from + // https://v8.dev/docs/stack-trace-api + 'getPosition', + 'getScriptNameOrSourceURL', + + 'toString', // TODO replace to use only whitelisted info +]; + +// TODO this is a ridiculously expensive way to attenuate callsites. +// Before that matters, we should switch to a reasonable representation. +const safeV8CallSiteFacet = callSite => { + const methodEntry = name => [name, () => callSite[name]()]; + const o = Object.fromEntries(safeV8CallSiteMethodNames.map(methodEntry)); + return Object.create(o, {}); +}; + +const safeV8SST = sst => sst.map(safeV8CallSiteFacet); + +const stackStringFromSST = (error, sst) => + [`${error}`, ...sst.map(callSite => `\n at ${callSite}`)].join(''); + +export function tameV8ErrorConstructor( + OriginalError, + InitialError, + errorTaming, +) { + // Mapping from error instance to the structured stack trace capturing the + // stack for that instance. + const ssts = new WeakMap(); + + // Use concise methods to obtain named functions without constructors. + const tamedMethods = { + // The optional `optFn` argument is for cutting off the bottom of + // the stack --- for capturing the stack only above the topmost + // call to that function. Since this isn't the "real" captureStackTrace + // but instead calls the real one, if no other cutoff is provided, + // we cut this one off. + captureStackTrace(error, optFn = tamedMethods.captureStackTrace) { + if (typeof OriginalError.captureStackTrace === 'function') { + // OriginalError.captureStackTrace is only on v8 + OriginalError.captureStackTrace(error, optFn); + return; + } + Reflect.set(error, 'stack', ''); + }, + // Shim of proposed special power, to reside by default only + // in the start compartment, for getting the stack traceback + // string associated with an error. + // See https://tc39.es/proposal-error-stacks/ + getStackString(error) { + if (!ssts.has(error)) { + // eslint-disable-next-line no-void + void error.stack; + } + const sst = ssts.get(error); + if (!sst) { + return ''; + } + return stackStringFromSST(error, sst); + }, + prepareStackTrace(error, sst) { + ssts.set(error, sst); + if (errorTaming === 'unsafe') { + return stackStringFromSST(error, sst); + } + return ''; + }, + }; + + // A prepareFn is a prepareStackTrace function. + // An sst is a `structuredStackTrace`, which is an array of + // callsites. + // A user prepareFn is a prepareFn defined by a client of this API, + // and provided by assigning to `Error.prepareStackTrace`. + // A user prepareFn should only receive an attenuated sst, which + // is an array of attenuated callsites. + // A system prepareFn is the prepareFn created by this module to + // be installed on the real `Error` constructor, to receive + // an original sst, i.e., an array of unattenuated callsites. + // An input prepareFn is a function the user assigns to + // `Error.prepareStackTrace`, which might be a user prepareFn or + // a system prepareFn previously obtained by reading + // `Error.prepareStackTrace`. + + const defaultPrepareFn = tamedMethods.prepareStackTrace; + + OriginalError.prepareStackTrace = defaultPrepareFn; + + // A weakset branding some functions as system prepareFns, all of which + // must be defined by this module, since they can receive an + // unattenuated sst. + const systemPrepareFnSet = new WeakSet([defaultPrepareFn]); + + const systemPrepareFnFor = inputPrepareFn => { + if (systemPrepareFnSet.has(inputPrepareFn)) { + return inputPrepareFn; + } + // Use concise methods to obtain named functions without constructors. + const systemMethods = { + prepareStackTrace(error, sst) { + ssts.set(error, sst); + return inputPrepareFn(error, safeV8SST(sst)); + }, + }; + systemPrepareFnSet.add(systemMethods.prepareStackTrace); + return systemMethods.prepareStackTrace; + }; + + defineProperties(InitialError, { + captureStackTrace: { + value: tamedMethods.captureStackTrace, + writable: true, + enumerable: false, + configurable: true, + }, + stackTraceLimit: { + get() { + if (typeof OriginalError.stackTraceLimit === 'number') { + // OriginalError.stackTraceLimit is only on v8 + return OriginalError.stackTraceLimit; + } + return undefined; + }, + // https://v8.dev/docs/stack-trace-api#compatibility advises that + // programmers can "always" set `Error.stackTraceLimit` and + // `Error.prepareStackTrace` even on non-v8 platforms. On non-v8 + // it will have no effect, but this advise only makes sense + // if the assignment itself does not fail, which it would + // if `Error` were naively frozen. Hence, we add setters that + // accept but ignore the assignment on non-v8 platforms. + set(newLimit) { + if (typeof OriginalError.stackTraceLimit === 'number') { + // OriginalError.stackTraceLimit is only on v8 + OriginalError.stackTraceLimit = newLimit; + // We place the useless return on the next line to ensure + // that anything we place after the if in the future only + // happens if the then-case does not. + // eslint-disable-next-line no-useless-return + return; + } + }, + // WTF on v8 stackTraceLimit is enumerable + enumerable: false, + configurable: true, + }, + prepareStackTrace: { + get() { + return OriginalError.prepareStackTrace; + }, + set(inputPrepareStackTraceFn) { + if (typeof inputPrepareStackTraceFn === 'function') { + const systemPrepareFn = systemPrepareFnFor(inputPrepareStackTraceFn); + OriginalError.prepareStackTrace = systemPrepareFn; + } else { + OriginalError.prepareStackTrace = defaultPrepareFn; + } + }, + enumerable: false, + configurable: true, + }, + }); + + return tamedMethods.getStackString; +} diff --git a/packages/ses/src/whitelist-intrinsics.js b/packages/ses/src/whitelist-intrinsics.js index 5937b7e772..d0e3a44f9b 100644 --- a/packages/ses/src/whitelist-intrinsics.js +++ b/packages/ses/src/whitelist-intrinsics.js @@ -24,33 +24,26 @@ // Typically, this module will not be used directly, but via the // [lockdown-shim] which handles all necessary repairs and taming in SES. // -// Differences with the Caja implementation -// -// Several improvements were made to reduce the complexity of the whitelisting -// algorithm and improve the accuracy and maintainability of the whitelist. -// -// 1. The indicators "maybeAccessor" and "\*" are replace with more explicit -// mapping. This removed the implicit recursive structure. -// -// 2. Instead, the `prototype`, `__proto__`, and `constructor` must be +// In the whitelist, the `prototype`, `__proto__`, and `constructor` must be // specified and point to top level entries in the map. For example, // `Object.__proto__` leads to `FunctionPrototype` which is a top level entry // in the map. // -// 3. The whitelist defines all prototype properties `\*Prototype` as top -// level entries. This creates a much more maintainable two level map, which is -// closer to how the language is specified. +// The permit value must be +// * the typeof name of a primitive for type-checking (for example, +// `Error.stackTraceLimit` leads to 'number'), +// * the name of an intrinsic, +// * an internal constant(for example, `eval` leads to `fn` which +// is an alias for `FunctionInstance`, a record that whitelist all +// properties allowed on such instance). +// * false, a property to be removed that we know about. // -// 4. The indicator `true` has been removed. Instead, the map value must be the -// name of a primitive for type-checking (for example, `Error.stackTraceLimit` -// leads to 'number'), the name of an intrinsic (for example, -// `ErrorPrototype.constructor` leads to 'Error'), or an internal constant (for -// example, `eval` leads to `fn` which is an alias for `FunctionInstance`, a -// record that whitelist all properties on allows on such instance). -// -// 5. In debug mode, all removed properties are listed to the console. +// All unlisted properties are also removed. But for the ones that are removed +// because they are unlisted, as opposed to `false`, we also print their +// name to the console as a useful diagnostic, possibly provoking an expansion +// of the whitelist. -import whitelist, { FunctionInstance } from './whitelist.js'; +import { whitelist, FunctionInstance, isAccessorPermit } from './whitelist.js'; import { getPrototypeOf, getOwnPropertyDescriptor } from './commons.js'; const { apply, ownKeys } = Reflect; @@ -86,6 +79,9 @@ export default function whitelistIntrinsics(intrinsics) { * Validate the object's [[prototype]] against a permit. */ function whitelistPrototype(path, obj, protoName) { + if (obj !== Object(obj)) { + throw new TypeError(`Object expected: ${path}, ${obj}, ${protoName}`); + } const proto = getPrototypeOf(obj); // Null prototype. @@ -98,13 +94,13 @@ export default function whitelistIntrinsics(intrinsics) { throw new TypeError(`Malformed whitelist permit ${path}.__proto__`); } - // If permit not specified, default tp Object.prototype. - if (proto === intrinsics[protoName || 'ObjectPrototype']) { + // If permit not specified, default to Object.prototype. + if (proto === intrinsics[protoName || '%ObjectPrototype%']) { return; } // We can't clean [[prototype]], therefore abort. - throw new Error(`Unexpected intrinsic ${path}.__proto__`); + throw new Error(`Unexpected intrinsic ${path}.__proto__ at ${protoName}`); } /** @@ -130,13 +126,16 @@ export default function whitelistIntrinsics(intrinsics) { if (prop === 'prototype' || prop === 'constructor') { // For prototype and constructor value properties, the permit - // is the mame of an intrinsic. + // is the name of an intrinsic. // Assumption: prototype and constructor cannot be primitives. - // Assert: the permit is the name of an untrinsic. + // Assert: the permit is the name of an intrinsic. // Assert: the property value is equal to that intrinsic. if (hasOwnProperty(intrinsics, permit)) { - return value === intrinsics[permit]; + if (value !== intrinsics[permit]) { + throw new TypeError(`Does not match whitelist ${path}`); + } + return true; } } else { // For all other properties, the permit is the name of a primitive. @@ -146,12 +145,17 @@ export default function whitelistIntrinsics(intrinsics) { // eslint-disable-next-line no-lonely-if if (primitives.includes(permit)) { // eslint-disable-next-line valid-typeof - return typeof value === permit; + if (typeof value !== permit) { + throw new TypeError( + `At ${path} expected ${permit} not ${typeof value}`, + ); + } + return true; } } } - throw new TypeError(`Unexpected whitelist permit ${path}`); + throw new TypeError(`Unexpected whitelist permit ${permit} at ${path}`); } /** @@ -163,9 +167,14 @@ export default function whitelistIntrinsics(intrinsics) { // Is this a value property? if (hasOwnProperty(desc, 'value')) { + if (isAccessorPermit(permit)) { + throw new TypeError(`Accessor expected at ${path}`); + } return isWhitelistPropertyValue(path, desc.value, prop, permit); } - + if (!isAccessorPermit(permit)) { + throw new TypeError(`Accessor not expected at ${path}`); + } return ( isWhitelistPropertyValue(`${path}`, desc.get, prop, permit.get) && isWhitelistPropertyValue(`${path}`, desc.set, prop, permit.set) @@ -176,13 +185,17 @@ export default function whitelistIntrinsics(intrinsics) { * getSubPermit() */ function getSubPermit(permit, prop) { - if (hasOwnProperty(permit, prop)) { - return permit[prop]; + const permitProp = prop === '__proto__' ? '--proto--' : prop; + if (hasOwnProperty(permit, permitProp)) { + return permit[permitProp]; } - if (permit['**proto**'] === 'FunctionPrototype') { - if (hasOwnProperty(FunctionInstance, prop)) { - return FunctionInstance[prop]; + // TODO Check both direct and indirect inheritance from + // %FunctionPrototype%, quickly. A naive check was too slow and was + // reverted. + if (permit['[[Proto]]'] === '%FunctionPrototype%') { + if (hasOwnProperty(FunctionInstance, permitProp)) { + return FunctionInstance[permitProp]; } } @@ -194,16 +207,10 @@ export default function whitelistIntrinsics(intrinsics) { * Whitelist all properties against a permit. */ function whitelistProperties(path, obj, permit) { - const protoName = permit['**proto**']; + const protoName = permit['[[Proto]]']; whitelistPrototype(path, obj, protoName); for (const prop of ownKeys(obj)) { - if (prop === '__proto__') { - // Ignore, already checked above. - // eslint-disable-next-line no-continue - continue; - } - const propString = asStringPropertyName(path, prop); const subPath = `${path}.${propString}`; const subPermit = getSubPermit(permit, propString); @@ -217,10 +224,12 @@ export default function whitelistIntrinsics(intrinsics) { } } - // console.log( - // `Removing ${subPath}`, - // Object.getOwnPropertyDescriptor(obj, prop), - // ); + if (subPermit !== false) { + // This call to `console.log` is intensional. It is not a vestige + // of a debugging attempt. See the comment at top of file for an + // explanation. + console.log(`Removing ${subPath}`); + } delete obj[prop]; } } diff --git a/packages/ses/src/whitelist.js b/packages/ses/src/whitelist.js index 08210acfdc..83b1e41520 100644 --- a/packages/ses/src/whitelist.js +++ b/packages/ses/src/whitelist.js @@ -6,6 +6,166 @@ * @author JF Paradis */ +/* eslint max-lines: 0 */ + +/** + * constantProperties + * non-configurable, non-writable data properties of all global objects. + * Must be powerless. + * Maps from property name to the actual value + */ +export const constantProperties = { + // *** 18.1 Value Properties of the Global Object + + Infinity, + NaN, + undefined, +}; + +/** + * universalPropertyNames + * Properties of all global objects. + * Must be powerless. + * Maps from property name to the intrinsic name in the whitelist. + */ +export const universalPropertyNames = { + // *** 18.2 Function Properties of the Global Object + + isFinite: 'isFinite', + isNaN: 'isNaN', + parseFloat: 'parseFloat', + parseInt: 'parseInt', + + decodeURI: 'decodeURI', + decodeURIComponent: 'decodeURIComponent', + encodeURI: 'encodeURI', + encodeURIComponent: 'encodeURIComponent', + + // *** 18.3 Constructor Properties of the Global Object + + Array: 'Array', + ArrayBuffer: 'ArrayBuffer', + BigInt: 'BigInt', + BigInt64Array: 'BigInt64Array', + BigUint64Array: 'BigUint64Array', + Boolean: 'Boolean', + DataView: 'DataView', + EvalError: 'EvalError', + Float32Array: 'Float32Array', + Float64Array: 'Float64Array', + Int8Array: 'Int8Array', + Int16Array: 'Int16Array', + Int32Array: 'Int32Array', + Map: 'Map', + Number: 'Number', + Object: 'Object', + Promise: 'Promise', + Proxy: 'Proxy', + RangeError: 'RangeError', + ReferenceError: 'ReferenceError', + Set: 'Set', + String: 'String', + Symbol: 'Symbol', + SyntaxError: 'SyntaxError', + TypeError: 'TypeError', + Uint8Array: 'Uint8Array', + Uint8ClampedArray: 'Uint8ClampedArray', + Uint16Array: 'Uint16Array', + Uint32Array: 'Uint32Array', + URIError: 'URIError', + WeakMap: 'WeakMap', + WeakSet: 'WeakSet', + + // *** 18.4 Other Properties of the Global Object + + JSON: 'JSON', + Reflect: 'Reflect', + + // *** Annex B + + escape: 'escape', + unescape: 'unescape', + + // ESNext + + lockdown: 'lockdown', + harden: 'harden', + StaticModuleRecord: 'StaticModuleRecord', +}; + +/** + * initialGlobalPropertyNames + * Those found only on the initial global, i.e., the global of the + * start compartment, as well as any compartments created before lockdown. + * These may provide much of the power provided by the original. + * Maps from property name to the intrinsic name in the whitelist. + */ +export const initialGlobalPropertyNames = { + // *** 18.3 Constructor Properties of the Global Object + + Date: '%InitialDate%', + Error: '%InitialError%', + RegExp: '%InitialRegExp%', + + // *** 18.4 Other Properties of the Global Object + + Math: '%InitialMath%', + + // ESNext + + // From Error-stack proposal + // Only on initial global. No corresponding + // powerless form for other globals. + getStackString: '%InitialGetStackString%', +}; + +/** + * sharedGlobalPropertyNames + * Those found only on the globals of new compartments created after lockdown, + * which must therefore be powerless. + * Maps from property name to the intrinsic name in the whitelist. + */ +export const sharedGlobalPropertyNames = { + // *** 18.3 Constructor Properties of the Global Object + + Date: '%SharedDate%', + Error: '%SharedError%', + RegExp: '%SharedRegExp%', + + // *** 18.4 Other Properties of the Global Object + + Math: '%SharedMath%', +}; + +/** + * uniqueGlobalPropertyNames + * Those made separately for each global, including the initial global + * of the start compartment. + * Maps from property name to the intrinsic name in the whitelist + * (which is currently always the same). + */ +export const uniqueGlobalPropertyNames = { + // *** 18.1 Value Properties of the Global Object + + globalThis: '%UniqueGlobalThis%', + + // *** 18.2 Function Properties of the Global Object + + eval: '%UniqueEval%', + + // *** 18.3 Constructor Properties of the Global Object + + Function: '%UniqueFunction%', + + // *** 18.4 Other Properties of the Global Object + + // ESNext + + Compartment: '%UniqueCompartment%', + // According to current agreements, eventually the Realm constructor too. + // 'Realm', +}; + /** *

Each JSON record enumerates the disposition of the properties on * some corresponding intrinsic object. @@ -35,19 +195,24 @@ * "Object"} may have and how each such property should be treated. * *

Notes: - *

  • "**proto**" is used to refer to "__proto__" without creating - * an actual prototype. - *
  • "ObjectPrototype" is the default "**proto**" (when not specified). + *
  • "[[Proto]]" is used to refer to the "[[Prototype]]" internal + * slot, which says which object this object inherits from. + *
  • "--proto--" is used to refer to the "__proto__" property name, + * which is the name of an accessor property on Object.prototype. + * In practice, it is used to access the [[Proto]] internal slot, + * but is distinct from the internal slot itself. We use + * "--proto--" rather than "__proto__" below because "__proto__" + * in an object literal is special syntax rather than a normal + * property definition. + *
  • "ObjectPrototype" is the default "[[Proto]]" (when not specified). *
  • Constants "fn" and "getter" are used to keep the structure DRY. *
  • Symbol properties are listed using the "@@name" form. */ -/* eslint max-lines: 0 */ - // 19.2.4 Function Instances export const FunctionInstance = { // Mentioned in "19.2.4.3 prototype" - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 19.2.4.1 length length: 'number', // 19.2.4.2 name @@ -59,10 +224,14 @@ export const FunctionInstance = { }; // 25.7.4 AsyncFunction Instances -export const AsyncFunctionInstance = { +const AsyncFunctionInstance = { // This property is not mentioned in ECMA 262, but is present in V8 and // necessary for lockdown to succeed. - '**proto**': 'AsyncFunctionPrototype', + // TODO Have `subPermit` check both direct and indirect inheritance + // from %FunctionPrototype%, so `subPermit` will also honor instance + // properties object that inherit from `%AsyncFunctionPrototype%` + + '[[Proto]]': '%AsyncFunctionPrototype%', // 25.7.4.1 length length: 'number', // 25.7.4.2 name @@ -79,7 +248,7 @@ const getter = { }; // Possible but not encountered in the specs -// const setter = { +// export const setter = { // get: 'undefined', // set: fn, // }; @@ -89,16 +258,22 @@ const accessor = { set: fn, }; +export function isAccessorPermit(permit) { + return permit === getter || permit === accessor; +} + // 19.5.6 NativeError Object Structure function NativeError(prototype) { return { // 19.5.6.2 Properties of the NativeError Constructors - '**proto**': 'Error', + '[[Proto]]': '%SharedError%', // 19.5.6.2.1 NativeError.prototype prototype, - // Add function instance properties to avoid mixin. + // TODO Have `subPermit` check both direct and indirect inheritance + // from %FunctionPrototype%, and then omit all these redundant + // occurrences of `length` and `name` // 19.2.4.1 length length: 'number', // 19.2.4.2 name @@ -109,7 +284,7 @@ function NativeError(prototype) { function NativeErrorPrototype(constructor) { return { // 19.5.6.3 Properties of the NativeError Prototype Objects - '**proto**': 'ErrorPrototype', + '[[Proto]]': '%ErrorPrototype%', // 19.5.6.3.1 NativeError.prototype.constructor constructor, // 19.5.6.3.2 NativeError.prototype.message @@ -125,8 +300,11 @@ function NativeErrorPrototype(constructor) { function TypedArray(prototype) { return { // 22.2.5 Properties of the TypedArray Constructors - '**proto**': 'TypedArray', + '[[Proto]]': '%TypedArray%', + // TODO Have `subPermit` check both direct and indirect inheritance + // from %FunctionPrototype%, and then omit all these redundant + // occurrences of `length` and `name` // Add function instance properties // 19.2.4.1 length length: 'number', @@ -143,7 +321,7 @@ function TypedArray(prototype) { function TypedArrayPrototype(constructor) { return { // 22.2.6 Properties of the TypedArray Prototype Objects - '**proto**': 'TypedArrayPrototype', + '[[Proto]]': '%TypedArrayPrototype%', // 22.2.6.1 TypedArray.prototype.BYTES_PER_ELEMENT BYTES_PER_ELEMENT: 'number', // 22.2.6.2TypedArray.prototype.constructor @@ -151,14 +329,105 @@ function TypedArrayPrototype(constructor) { }; } -export default { +// Without Math.random +const SharedMath = { + // 20.3.1.1 Math.E + E: 'number', + // 20.3.1.2 Math.LN10 + LN10: 'number', + // 20.3.1.3 Math.LN2 + LN2: 'number', + // 20.3.1.4 Math.LOG10E + LOG10E: 'number', + // 20.3.1.5 Math.LOG2E + LOG2E: 'number', + // 20.3.1.6 Math.PI + PI: 'number', + // 20.3.1.7 Math.SQRT1_2 + SQRT1_2: 'number', + // 20.3.1.8 Math.SQRT2 + SQRT2: 'number', + // 20.3.1.9 Math [ @@toStringTag ] + '@@toStringTag': 'string', + // 20.3.2.1 Math.abs + abs: fn, + // 20.3.2.2 Math.acos + acos: fn, + // 20.3.2.3 Math.acosh + acosh: fn, + // 20.3.2.4 Math.asin + asin: fn, + // 20.3.2.5 Math.asinh + asinh: fn, + // 20.3.2.6 Math.atan + atan: fn, + // 20.3.2.7 Math.atanh + atanh: fn, + // 20.3.2.8 Math.atan2 + atan2: fn, + // 20.3.2.9 Math.cbrt + cbrt: fn, + // 20.3.2.10 Math.ceil + ceil: fn, + // 20.3.2.11 Math.clz32 + clz32: fn, + // 20.3.2.12 Math.cos + cos: fn, + // 20.3.2.13 Math.cosh + cosh: fn, + // 20.3.2.14 Math.exp + exp: fn, + // 20.3.2.15 Math.expm1 + expm1: fn, + // 20.3.2.16 Math.floor + floor: fn, + // 20.3.2.17 Math.fround + fround: fn, + // 20.3.2.18 Math.hypot + hypot: fn, + // 20.3.2.19 Math.imul + imul: fn, + // 20.3.2.20 Math.log + log: fn, + // 20.3.2.21 Math.log1p + log1p: fn, + // 20.3.2.22 Math.log10 + log10: fn, + // 20.3.2.23 Math.log2 + log2: fn, + // 20.3.2.24 Math.max + max: fn, + // 20.3.2.25 Math.min + min: fn, + // 20.3.2.26Math.pow + pow: fn, + // 20.3.2.28 Math.round + round: fn, + // 20.3.2.29 Math.sign + sign: fn, + // 20.3.2.30 Math.sin + sin: fn, + // 20.3.2.31 Math.sinh + sinh: fn, + // 20.3.2.32 Math.sqrt + sqrt: fn, + // 20.3.2.33 Math.tan + tan: fn, + // 20.3.2.34 Math.tanh + tanh: fn, + // 20.3.2.35 Math.trunc + trunc: fn, + // 20.3.2.35Math.trunc +}; + +export const whitelist = { // ECMA https://tc39.es/ecma262 // The intrinsics object has no prototype to avoid conflicts. - '**proto**': null, + '[[Proto]]': null, // 9.2.4.1 %ThrowTypeError% - ThrowTypeError: fn, + '%ThrowTypeError%': fn, // *** 18 The Global Object @@ -174,7 +443,7 @@ export default { // *** 18.2 Function Properties of the Global Object // 18.2.1 eval - eval: fn, + '%UniqueEval%': fn, // 18.2.2 isFinite isFinite: fn, // 18.2.3 isNaN @@ -196,12 +465,12 @@ export default { Object: { // 19.1.2 Properties of the Object Constructor - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 19.1.2.1 Object.assign assign: fn, // 19.1.2.2 Object.create create: fn, - // 19.1.2.3 Object.definePropertie + // 19.1.2.3 Object.defineProperties defineProperties: fn, // 19.1.2.4 Object.defineProperty defineProperty: fn, @@ -234,7 +503,7 @@ export default { // 19.1.2.18 Object.preventExtensions preventExtensions: fn, // 19.1.2.19 Object.prototype - prototype: 'ObjectPrototype', + prototype: '%ObjectPrototype%', // 19.1.2.20 Object.seal seal: fn, // 19.1.2.21 Object.setPrototypeOf @@ -243,9 +512,9 @@ export default { values: fn, }, - ObjectPrototype: { + '%ObjectPrototype%': { // 19.1.3 Properties of the Object Prototype Object - '**proto**': null, + '[[Proto]]': null, // 19.1.3.1 Object.prototype.constructor constructor: 'Object', // 19.1.3.2 Object.prototype.hasOwnProperty @@ -264,7 +533,7 @@ export default { // B.2.2 Additional Properties of the Object.prototype Object // B.2.2.1 Object.prototype.__proto__ - // '**proto**': accessor, // TODO(markm) + '--proto--': accessor, // B.2.2.2 Object.prototype.__defineGetter__ __defineGetter__: fn, // B.2.2.3 Object.prototype.__defineSetter__ @@ -275,16 +544,22 @@ export default { __lookupSetter__: fn, }, - Function: { + '%UniqueFunction%': { // 19.2.2 Properties of the Function Constructor - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 19.2.2.1 Function.length length: 'number', // 19.2.2.2 Function.prototype - prototype: 'FunctionPrototype', + prototype: '%FunctionPrototype%', }, - FunctionPrototype: { + '%InertFunction%': { + '[[Proto]]': '%FunctionPrototype%', + length: 'number', + prototype: '%FunctionPrototype%', + }, + + '%FunctionPrototype%': { // 19.2.3 Properties of the Function Prototype Object length: 'number', name: 'string', @@ -295,21 +570,24 @@ export default { // 19.2.3.3 Function.prototype.call call: fn, // 19.2.3.4 Function.prototype.constructor - constructor: 'FunctionPrototypeConstructor', // TODO test + constructor: '%InertFunction%', // TODO test // 19.2.3.5 Function.prototype.toString toString: fn, // 19.2.3.6 Function.prototype [ @@hasInstance ] '@@hasInstance': fn, + // non-std yet but proposed. To be removed if there + caller: false, + arguments: false, }, Boolean: { // 19.3.2 Properties of the Boolean Constructor - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 19.3.2.1 Boolean.prototype - prototype: 'BooleanPrototype', + prototype: '%BooleanPrototype%', }, - BooleanPrototype: { + '%BooleanPrototype%': { // 19.3.3.1 Boolean.prototype.constructor constructor: 'Boolean', // 19.3.3.2 Boolean.prototype.toString @@ -320,7 +598,7 @@ export default { Symbol: { // 19.4.2 Properties of the Symbol Constructor - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 19.4.2.1 Symbol.asyncIterator asyncIterator: 'symbol', // 19.4.2.2 Symbol.for @@ -338,7 +616,7 @@ export default { // 19.4.2.8 Symbol.matchAll matchAll: 'symbol', // 19.4.2.9 Symbol.prototype - prototype: 'SymbolPrototype', + prototype: '%SymbolPrototype%', // 19.4.2.10 Symbol.replace replace: 'symbol', // 19.4.2.11 Symbol.search @@ -355,7 +633,7 @@ export default { unscopables: 'symbol', }, - SymbolPrototype: { + '%SymbolPrototype%': { // 19.4.3 Properties of the Symbol Prototype Object // 19.4.3.1 Symbol.prototype.constructor @@ -372,11 +650,11 @@ export default { '@@toStringTag': 'string', }, - Error: { + '%InitialError%': { // 19.5.2 Properties of the Error Constructor - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 19.5.2.1 Error.prototype - prototype: 'ErrorPrototype', + prototype: '%ErrorPrototype%', // Non standard, v8 only, used by tap captureStackTrace: fn, // Non standard, v8 only, used by tap, tamed to accessor @@ -385,9 +663,22 @@ export default { prepareStackTrace: accessor, }, - ErrorPrototype: { + '%SharedError%': { + // 19.5.2 Properties of the Error Constructor + '[[Proto]]': '%FunctionPrototype%', + // 19.5.2.1 Error.prototype + prototype: '%ErrorPrototype%', + // Non standard, v8 only, used by tap + captureStackTrace: fn, + // Non standard, v8 only, used by tap, tamed to accessor + stackTraceLimit: accessor, + // Non standard, v8 only, used by several, tamed to accessor + prepareStackTrace: accessor, + }, + + '%ErrorPrototype%': { // 19.5.3.1 Error.prototype.constructor - constructor: 'Error', + constructor: '%SharedError%', // 19.5.3.2 Error.prototype.message message: 'string', // 19.5.3.3 Error.prototype.name @@ -400,25 +691,25 @@ export default { // 19.5.6.1.1 NativeError - EvalError: NativeError('EvalErrorPrototype'), - RangeError: NativeError('RangeErrorPrototype'), - ReferenceError: NativeError('ReferenceErrorPrototype'), - SyntaxError: NativeError('SyntaxErrorPrototype'), - TypeError: NativeError('TypeErrorPrototype'), - URIError: NativeError('URIErrorPrototype'), + EvalError: NativeError('%EvalErrorPrototype%'), + RangeError: NativeError('%RangeErrorPrototype%'), + ReferenceError: NativeError('%ReferenceErrorPrototype%'), + SyntaxError: NativeError('%SyntaxErrorPrototype%'), + TypeError: NativeError('%TypeErrorPrototype%'), + URIError: NativeError('%URIErrorPrototype%'), - EvalErrorPrototype: NativeErrorPrototype('EvalError'), - RangeErrorPrototype: NativeErrorPrototype('RangeError'), - ReferenceErrorPrototype: NativeErrorPrototype('ReferenceError'), - SyntaxErrorPrototype: NativeErrorPrototype('SyntaxError'), - TypeErrorPrototype: NativeErrorPrototype('TypeError'), - URIErrorPrototype: NativeErrorPrototype('URIError'), + '%EvalErrorPrototype%': NativeErrorPrototype('EvalError'), + '%RangeErrorPrototype%': NativeErrorPrototype('RangeError'), + '%ReferenceErrorPrototype%': NativeErrorPrototype('ReferenceError'), + '%SyntaxErrorPrototype%': NativeErrorPrototype('SyntaxError'), + '%TypeErrorPrototype%': NativeErrorPrototype('TypeError'), + '%URIErrorPrototype%': NativeErrorPrototype('URIError'), // *** 20 Numbers and Dates Number: { // 20.1.2 Properties of the Number Constructor - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 20.1.2.1 Number.EPSILON EPSILON: 'number', // 20.1.2.2 Number.isFinite @@ -448,10 +739,10 @@ export default { // 20.1.2.14 Number.POSITIVE_INFINITY POSITIVE_INFINITY: 'number', // 20.1.2.15 Number.prototype - prototype: 'NumberPrototype', + prototype: '%NumberPrototype%', }, - NumberPrototype: { + '%NumberPrototype%': { // 20.1.3 Properties of the Number Prototype Object // 20.1.3.1 Number.prototype.constructor @@ -472,16 +763,16 @@ export default { BigInt: { // 20.2.2Properties of the BigInt Constructor - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 20.2.2.1 BigInt.asIntN asIntN: fn, // 20.2.2.2 BigInt.asUintN asUintN: fn, // 20.2.2.3 BigInt.prototype - prototype: 'BigIntPrototype', + prototype: '%BigIntPrototype%', }, - BigIntPrototype: { + '%BigIntPrototype%': { // 20.2.3.1 BigInt.prototype.constructor constructor: 'BigInt', // 20.2.3.2 BigInt.prototype.toLocaleString @@ -494,114 +785,43 @@ export default { '@@toStringTag': 'string', }, - Math: { - // 20.3.1.1 Math.E - E: 'number', - // 20.3.1.2 Math.LN10 - LN10: 'number', - // 20.3.1.3 Math.LN2 - LN2: 'number', - // 20.3.1.4 Math.LOG10E - LOG10E: 'number', - // 20.3.1.5 Math.LOG2E - LOG2E: 'number', - // 20.3.1.6 Math.PI - PI: 'number', - // 20.3.1.7 Math.SQRT1_2 - SQRT1_2: 'number', - // 20.3.1.8 Math.SQRT2 - SQRT2: 'number', - // 20.3.1.9 Math [ @@toStringTag ] - '@@toStringTag': 'string', - // 20.3.2.1 Math.abs - abs: fn, - // 20.3.2.2 Math.acos - acos: fn, - // 20.3.2.3 Math.acosh - acosh: fn, - // 20.3.2.4 Math.asin - asin: fn, - // 20.3.2.5 Math.asinh - asinh: fn, - // 20.3.2.6 Math.atan - atan: fn, - // 20.3.2.7 Math.atanh - atanh: fn, - // 20.3.2.8 Math.atan2 - atan2: fn, - // 20.3.2.9 Math.cbrt - cbrt: fn, - // 20.3.2.10 Math.ceil - ceil: fn, - // 20.3.2.11 Math.clz32 - clz32: fn, - // 20.3.2.12 Math.cos - cos: fn, - // 20.3.2.13 Math.cosh - cosh: fn, - // 20.3.2.14 Math.exp - exp: fn, - // 20.3.2.15 Math.expm1 - expm1: fn, - // 20.3.2.16 Math.floor - floor: fn, - // 20.3.2.17 Math.fround - fround: fn, - // 20.3.2.18 Math.hypot - hypot: fn, - // 20.3.2.19 Math.imul - imul: fn, - // 20.3.2.20 Math.log - log: fn, - // 20.3.2.21 Math.log1p - log1p: fn, - // 20.3.2.22 Math.log10 - log10: fn, - // 20.3.2.23 Math.log2 - log2: fn, - // 20.3.2.24 Math.max - max: fn, - // 20.3.2.25 Math.min - min: fn, - // 20.3.2.26Math.pow - pow: fn, + '%InitialMath%': { + ...SharedMath, // 20.3.2.27Math.random random: fn, - // 20.3.2.28 Math.round - round: fn, - // 20.3.2.29 Math.sign - sign: fn, - // 20.3.2.30 Math.sin - sin: fn, - // 20.3.2.31 Math.sinh - sinh: fn, - // 20.3.2.32 Math.sqrt - sqrt: fn, - // 20.3.2.33 Math.tan - tan: fn, - // 20.3.2.34 Math.tanh - tanh: fn, - // 20.3.2.35 Math.trunc - trunc: fn, - // 20.3.2.35Math.trunc - }, - - Date: { + }, + + '%SharedMath%': SharedMath, + + '%InitialDate%': { // 20.4.3 Properties of the Date Constructor - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 20.4.3.1 Date.now now: fn, // 20.4.3.2 Date.parse parse: fn, // 20.4.3.3 Date.prototype - prototype: 'DatePrototype', + prototype: '%DatePrototype%', // 20.4.3.4 Date.UTC UTC: fn, }, - DatePrototype: { + '%SharedDate%': { + // 20.4.3 Properties of the Date Constructor + '[[Proto]]': '%FunctionPrototype%', + // 20.4.3.1 Date.now + now: fn, + // 20.4.3.2 Date.parse + parse: fn, + // 20.4.3.3 Date.prototype + prototype: '%DatePrototype%', + // 20.4.3.4 Date.UTC + UTC: fn, + }, + + '%DatePrototype%': { // 20.4.4.1 Date.prototype.constructor - constructor: 'Date', + constructor: '%SharedDate%', // 20.4.4.2 Date.prototype.getDate getDate: fn, // 20.4.4.3 Date.prototype.getDay @@ -705,18 +925,18 @@ export default { String: { // 21.1.2 Properties of the String Constructor - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 21.1.2.1 String.fromCharCode fromCharCode: fn, // 21.1.2.2 String.fromCodePoint fromCodePoint: fn, // 21.1.2.3 String.prototype - prototype: 'StringPrototype', + prototype: '%StringPrototype%', // 21.1.2.4 String.raw raw: fn, }, - StringPrototype: { + '%StringPrototype%': { // 21.1.3 Properties of the String Prototype Object length: 'number', // 21.1.3.1 String.prototype.charAt @@ -820,28 +1040,59 @@ export default { trimRight: fn, }, - StringIteratorPrototype: { + '%StringIteratorPrototype%': { // 21.1.5.2 he %StringIteratorPrototype% Object - '**proto**': 'IteratorPrototype', + '[[Proto]]': '%IteratorPrototype%', // 21.1.5.2.1 %StringIteratorPrototype%.next ( ) next: fn, // 21.1.5.2.2 %StringIteratorPrototype% [ @@toStringTag ] '@@toStringTag': 'string', }, - RegExp: { + '%InitialRegExp%': { // 21.2.4 Properties of the RegExp Constructor - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 21.2.4.1 RegExp.prototype - prototype: 'RegExpPrototype', + prototype: '%RegExpPrototype%', // 21.2.4.2 get RegExp [ @@species ] '@@species': getter, + + // The https://github.com/tc39/proposal-regexp-legacy-features + // are all optional, unsafe, and omitted + input: false, + $_: false, + lastMatch: false, + '$&': false, + lastParen: false, + '$+': false, + leftContext: false, + '$`': false, + rightContext: false, + "$'": false, + $1: false, + $2: false, + $3: false, + $4: false, + $5: false, + $6: false, + $7: false, + $8: false, + $9: false, }, - RegExpPrototype: { + '%SharedRegExp%': { + // 21.2.4 Properties of the RegExp Constructor + '[[Proto]]': '%FunctionPrototype%', + // 21.2.4.1 RegExp.prototype + prototype: '%RegExpPrototype%', + // 21.2.4.2 get RegExp [ @@species ] + '@@species': getter, + }, + + '%RegExpPrototype%': { // 21.2.5 Properties of the RegExp Prototype Object // 21.2.5.1 RegExp.prototype.constructor - constructor: 'RegExp', + constructor: '%SharedRegExp%', // 21.2.5.2 RegExp.prototype.exec exec: fn, // 21.2.5.3 get RegExp.prototype.dotAll @@ -881,9 +1132,9 @@ export default { compile: false, // UNSAFE and suppressed. }, - RegExpStringIteratorPrototype: { + '%RegExpStringIteratorPrototype%': { // 21.2.7.1 The %RegExpStringIteratorPrototype% Object - '**proto**': 'IteratorPrototype', + '[[Proto]]': '%IteratorPrototype%', // 21.2.7.1.1 %RegExpStringIteratorPrototype%.next next: fn, // 21.2.7.1.2 %RegExpStringIteratorPrototype% [ @@toStringTag ] @@ -894,7 +1145,7 @@ export default { Array: { // 22.1.2 Properties of the Array Constructor - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 22.1.2.1 Array.from from: fn, // 22.1.2.2 Array.isArray @@ -902,12 +1153,12 @@ export default { // 22.1.2.3 Array.of of: fn, // 22.1.2.4 Array.prototype - prototype: 'ArrayPrototype', + prototype: '%ArrayPrototype%', // 22.1.2.5 get Array [ @@species ] '@@species': getter, }, - ArrayPrototype: { + '%ArrayPrototype%': { // 22.1.3 Properties of the Array Prototype Object length: 'number', // 22.1.3.1 Array.prototype.concat @@ -977,9 +1228,8 @@ export default { // 22.1.3.33 Array.prototype [ @@iterator ] '@@iterator': fn, // 22.1.3.34 Array.prototype [ @@unscopables ] - // TODO what? '@@unscopables': { - '**proto**': null, + '[[Proto]]': null, copyWithin: 'boolean', entries: 'boolean', fill: 'boolean', @@ -993,9 +1243,9 @@ export default { }, }, - ArrayIteratorPrototype: { + '%ArrayIteratorPrototype%': { // 22.1.5.2 The %ArrayIteratorPrototype% Object - '**proto**': 'IteratorPrototype', + '[[Proto]]': '%IteratorPrototype%', // 22.1.5.2.1 %ArrayIteratorPrototype%.next next: fn, // 22.1.5.2.2 %ArrayIteratorPrototype% [ @@toStringTag ] @@ -1004,20 +1254,20 @@ export default { // *** 22.2 TypedArray Objects - TypedArray: { + '%TypedArray%': { // 22.2.2 Properties of the %TypedArray% Intrinsic Object - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 22.2.2.1 %TypedArray%.from from: fn, // 22.2.2.2 %TypedArray%.of of: fn, // 22.2.2.3 %TypedArray%.prototype - prototype: 'TypedArrayPrototype', + prototype: '%TypedArrayPrototype%', // 22.2.2.4 get %TypedArray% [ @@species ] '@@species': getter, }, - TypedArrayPrototype: { + '%TypedArrayPrototype%': { // 22.2.3.1 get %TypedArray%.prototype.buffer buffer: getter, // 22.2.3.2 get %TypedArray%.prototype.byteLength @@ -1025,7 +1275,7 @@ export default { // 22.2.3.3 get %TypedArray%.prototype.byteOffset byteOffset: getter, // 22.2.3.4 %TypedArray%.prototype.constructor - constructor: 'TypedArray', + constructor: '%TypedArray%', // 22.2.3.5 %TypedArray%.prototype.copyWithin copyWithin: fn, // 22.2.3.6 %TypedArray%.prototype.entries @@ -1086,41 +1336,41 @@ export default { // 22.2.4 The TypedArray Constructors - BigInt64Array: TypedArray('BigInt64ArrayPrototype'), - BigUint64Array: TypedArray('BigUint64ArrayPrototype'), - Float32Array: TypedArray('Float32ArrayPrototype'), - Float64Array: TypedArray('Float64ArrayPrototype'), - Int16Array: TypedArray('Int16ArrayPrototype'), - Int32Array: TypedArray('Int32ArrayPrototype'), - Int8Array: TypedArray('Int8ArrayPrototype'), - Uint16Array: TypedArray('Uint16ArrayPrototype'), - Uint32Array: TypedArray('Uint32ArrayPrototype'), - Uint8Array: TypedArray('Uint8ArrayPrototype'), - Uint8ClampedArray: TypedArray('Uint8ClampedArrayPrototype'), - - BigInt64ArrayPrototype: TypedArrayPrototype('BigInt64Array'), - BigUint64ArrayPrototype: TypedArrayPrototype('BigUint64Array'), - Float32ArrayPrototype: TypedArrayPrototype('Float32Array'), - Float64ArrayPrototype: TypedArrayPrototype('Float64Array'), - Int16ArrayPrototype: TypedArrayPrototype('Int16Array'), - Int32ArrayPrototype: TypedArrayPrototype('Int32Array'), - Int8ArrayPrototype: TypedArrayPrototype('Int8Array'), - Uint16ArrayPrototype: TypedArrayPrototype('Uint16Array'), - Uint32ArrayPrototype: TypedArrayPrototype('Uint32Array'), - Uint8ArrayPrototype: TypedArrayPrototype('Uint8Array'), - Uint8ClampedArrayPrototype: TypedArrayPrototype('Uint8ClampedArray'), + BigInt64Array: TypedArray('%BigInt64ArrayPrototype%'), + BigUint64Array: TypedArray('%BigUint64ArrayPrototype%'), + Float32Array: TypedArray('%Float32ArrayPrototype%'), + Float64Array: TypedArray('%Float64ArrayPrototype%'), + Int16Array: TypedArray('%Int16ArrayPrototype%'), + Int32Array: TypedArray('%Int32ArrayPrototype%'), + Int8Array: TypedArray('%Int8ArrayPrototype%'), + Uint16Array: TypedArray('%Uint16ArrayPrototype%'), + Uint32Array: TypedArray('%Uint32ArrayPrototype%'), + Uint8Array: TypedArray('%Uint8ArrayPrototype%'), + Uint8ClampedArray: TypedArray('%Uint8ClampedArrayPrototype%'), + + '%BigInt64ArrayPrototype%': TypedArrayPrototype('BigInt64Array'), + '%BigUint64ArrayPrototype%': TypedArrayPrototype('BigUint64Array'), + '%Float32ArrayPrototype%': TypedArrayPrototype('Float32Array'), + '%Float64ArrayPrototype%': TypedArrayPrototype('Float64Array'), + '%Int16ArrayPrototype%': TypedArrayPrototype('Int16Array'), + '%Int32ArrayPrototype%': TypedArrayPrototype('Int32Array'), + '%Int8ArrayPrototype%': TypedArrayPrototype('Int8Array'), + '%Uint16ArrayPrototype%': TypedArrayPrototype('Uint16Array'), + '%Uint32ArrayPrototype%': TypedArrayPrototype('Uint32Array'), + '%Uint8ArrayPrototype%': TypedArrayPrototype('Uint8Array'), + '%Uint8ClampedArrayPrototype%': TypedArrayPrototype('Uint8ClampedArray'), // *** 23 Keyed Collections Map: { // 23.1.2 Properties of the Map Constructor - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 23.2.2.2 get Set [ @@species ] '@@species': getter, - prototype: 'MapPrototype', + prototype: '%MapPrototype%', }, - MapPrototype: { + '%MapPrototype%': { // 23.1.3.1 Map.prototype.clear clear: fn, // 23.1.3.2 Map.prototype.constructor @@ -1149,9 +1399,9 @@ export default { '@@toStringTag': 'string', }, - MapIteratorPrototype: { + '%MapIteratorPrototype%': { // 23.1.5.2 The %MapIteratorPrototype% Object - '**proto**': 'IteratorPrototype', + '[[Proto]]': '%IteratorPrototype%', // 23.1.5.2.1 %MapIteratorPrototype%.next next: fn, // 23.1.5.2.2 %MapIteratorPrototype% [ @@toStringTag ] @@ -1160,14 +1410,14 @@ export default { Set: { // 23.2.2 Properties of the Set Constructor - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 23.2.2.1 Set.prototype - prototype: 'SetPrototype', + prototype: '%SetPrototype%', // 23.2.2.2 get Set [ @@species ] '@@species': getter, }, - SetPrototype: { + '%SetPrototype%': { // 23.2.3.1 Set.prototype.add add: fn, // 23.2.3.2 Set.prototype.clear @@ -1194,9 +1444,9 @@ export default { '@@toStringTag': 'string', }, - SetIteratorPrototype: { + '%SetIteratorPrototype%': { // 23.2.5.2 The %SetIteratorPrototype% Object - '**proto**': 'IteratorPrototype', + '[[Proto]]': '%IteratorPrototype%', // 23.2.5.2.1 %SetIteratorPrototype%.next next: fn, // 23.2.5.2.2 %SetIteratorPrototype% [ @@toStringTag ] @@ -1205,12 +1455,12 @@ export default { WeakMap: { // 23.3.2 Properties of the WeakMap Constructor - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 23.3.2.1 WeakMap.prototype - prototype: 'WeakMapPrototype', + prototype: '%WeakMapPrototype%', }, - WeakMapPrototype: { + '%WeakMapPrototype%': { // 23.3.3.1 WeakMap.prototype.constructor constructor: 'WeakMap', // 23.3.3.2 WeakMap.prototype.delete @@ -1227,12 +1477,12 @@ export default { WeakSet: { // 23.4.2Properties of the WeakSet Constructor - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 23.4.2.1 WeakSet.prototype - prototype: 'WeakSetPrototype', + prototype: '%WeakSetPrototype%', }, - WeakSetPrototype: { + '%WeakSetPrototype%': { // 23.4.3.1 WeakSet.prototype.add add: fn, // 23.4.3.2 WeakSet.prototype.constructor @@ -1249,16 +1499,16 @@ export default { ArrayBuffer: { // 24.1.3 Properties of the ArrayBuffer Constructor - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 24.1.3.1 ArrayBuffer.isView isView: fn, // 24.1.3.2 ArrayBuffer.prototype - prototype: 'ArrayBufferPrototype', + prototype: '%ArrayBufferPrototype%', // 24.1.3.3 get ArrayBuffer [ @@species ] '@@species': getter, }, - ArrayBufferPrototype: { + '%ArrayBufferPrototype%': { // 24.1.4.1 get ArrayBuffer.prototype.byteLength byteLength: getter, // 24.1.4.2 ArrayBuffer.prototype.constructor @@ -1271,15 +1521,16 @@ export default { // 24.2 SharedArrayBuffer Objects SharedArrayBuffer: false, // UNSAFE and purposely suppressed. + '%SharedArrayBufferPrototype%': false, // UNSAFE and purposely suppressed. DataView: { // 24.3.3 Properties of the DataView Constructor - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 24.3.3.1 DataView.prototype - prototype: 'DataViewPrototype', + prototype: '%DataViewPrototype%', }, - DataViewPrototype: { + '%DataViewPrototype%': { // 24.3.4.1 get DataView.prototype.buffer buffer: getter, // 24.3.4.2 get DataView.prototype.byteLength @@ -1346,63 +1597,69 @@ export default { // *** 25 Control Abstraction Objects - IteratorPrototype: { + '%IteratorPrototype%': { // 25.1.2 The %IteratorPrototype% Object // 25.1.2.1 %IteratorPrototype% [ @@iterator ] '@@iterator': fn, }, - AsyncIteratorPrototype: { + '%AsyncIteratorPrototype%': { // 25.1.3 The %AsyncIteratorPrototype% Object // 25.1.3.1 %AsyncIteratorPrototype% [ @@asyncIterator ] '@@asyncIterator': fn, }, - GeneratorFunction: { + '%InertGeneratorFunction%': { // 25.2.2 Properties of the GeneratorFunction Constructor - '**proto**': 'FunctionPrototypeConstructor', + '[[Proto]]': '%InertFunction%', name: 'string', // 25.2.2.1 GeneratorFunction.length length: 'number', // 25.2.2.2 GeneratorFunction.prototype - prototype: 'Generator', + prototype: '%Generator%', + // Non-standard but seen on v8. Probably harmless, but useless. + toString: false, }, - Generator: { + '%Generator%': { // 25.2.3 Properties of the GeneratorFunction Prototype Object - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 25.2.3.1 GeneratorFunction.prototype.constructor - constructor: 'GeneratorFunction', + constructor: '%InertGeneratorFunction%', // 25.2.3.2 GeneratorFunction.prototype.prototype - prototype: 'GeneratorPrototype', + prototype: '%GeneratorPrototype%', + // 25.2.3.3 GeneratorFunction.prototype [ @@toStringTag ] + '@@toStringTag': 'string', }, - AsyncGeneratorFunction: { + '%InertAsyncGeneratorFunction%': { // 25.3.2 Properties of the AsyncGeneratorFunction Constructor - '**proto**': 'FunctionPrototypeConstructor', + '[[Proto]]': '%InertFunction%', name: 'string', // 25.3.2.1 AsyncGeneratorFunction.length length: 'number', // 25.3.2.2 AsyncGeneratorFunction.prototype - prototype: 'AsyncGenerator', + prototype: '%AsyncGenerator%', + // Non-standard but seen on v8. Probably harmless, but useless. + toString: false, }, - AsyncGenerator: { + '%AsyncGenerator%': { // 25.3.3 Properties of the AsyncGeneratorFunction Prototype Object - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 25.3.3.1 AsyncGeneratorFunction.prototype.constructor - constructor: 'AsyncGeneratorFunction', + constructor: '%InertAsyncGeneratorFunction%', // 25.3.3.2 AsyncGeneratorFunction.prototype.prototype - prototype: 'AsyncGeneratorPrototype', + prototype: '%AsyncGeneratorPrototype%', // 25.3.3.3 AsyncGeneratorFunction.prototype [ @@toStringTag ] '@@toStringTag': 'string', }, - GeneratorPrototype: { + '%GeneratorPrototype%': { // 25.4.1 Properties of the Generator Prototype Object - '**proto**': 'IteratorPrototype', + '[[Proto]]': '%IteratorPrototype%', // 25.4.1.1 Generator.prototype.constructor - constructor: 'Generator', + constructor: '%Generator%', // 25.4.1.2 Generator.prototype.next next: fn, // 25.4.1.3 Generator.prototype.return @@ -1413,11 +1670,11 @@ export default { '@@toStringTag': 'string', }, - AsyncGeneratorPrototype: { + '%AsyncGeneratorPrototype%': { // 25.5.1 Properties of the AsyncGenerator Prototype Object - '**proto**': 'AsyncIteratorPrototype', + '[[Proto]]': '%AsyncIteratorPrototype%', // 25.5.1.1 AsyncGenerator.prototype.constructor - constructor: 'AsyncGenerator', + constructor: '%AsyncGenerator%', // 25.5.1.2 AsyncGenerator.prototype.next next: fn, // 25.5.1.3 AsyncGenerator.prototype.return @@ -1430,13 +1687,13 @@ export default { Promise: { // 25.6.4 Properties of the Promise Constructor - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 25.6.4.1 Promise.all all: fn, // 25.6.4.2 Promise.allSettled allSettled: fn, // 25.6.4.3Promise.prototype - prototype: 'PromisePrototype', + prototype: '%PromisePrototype%', // 25.6.4.4 Promise.race race: fn, // 25.6.4.5 Promise.reject @@ -1447,7 +1704,7 @@ export default { '@@species': getter, }, - PromisePrototype: { + '%PromisePrototype%': { // 25.6.5 Properties of the Promise Prototype Object // 25.6.5.1 Promise.prototype.catch catch: fn, @@ -1461,21 +1718,23 @@ export default { '@@toStringTag': 'string', }, - AsyncFunction: { + '%InertAsyncFunction%': { // 25.7.2 Properties of the AsyncFunction Constructor - '**proto**': 'FunctionPrototypeConstructor', + '[[Proto]]': '%InertFunction%', name: 'string', // 25.7.2.1 AsyncFunction.length length: 'number', // 25.7.2.2 AsyncFunction.prototype - prototype: 'AsyncFunctionPrototype', + prototype: '%AsyncFunctionPrototype%', + // Non-standard but seen on v8. Probably harmless, but useless. + toString: false, }, - AsyncFunctionPrototype: { + '%AsyncFunctionPrototype%': { // 25.7.3 Properties of the AsyncFunction Prototype Object - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 25.7.3.1 AsyncFunction.prototype.constructor - constructor: 'AsyncFunction', + constructor: '%InertAsyncFunction%', // 25.7.3.2 AsyncFunction.prototype [ @@toStringTag ] '@@toStringTag': 'string', }, @@ -1515,7 +1774,7 @@ export default { Proxy: { // 26.2.2 Properties of the Proxy Constructor - '**proto**': 'FunctionPrototype', + '[[Proto]]': '%FunctionPrototype%', // 26.2.2.1 Proxy.revocable revocable: fn, }, @@ -1531,35 +1790,49 @@ export default { // ESNext - // New intrinsic like %Function% but disabled. - FunctionPrototypeConstructor: { - '**proto**': 'FunctionPrototype', - length: 'number', - prototype: 'FunctionPrototype', + '%UniqueCompartment%': { + '[[Proto]]': '%FunctionPrototype%', + prototype: '%CompartmentPrototype%', + toString: fn, }, - Compartment: { - '**proto**': 'FunctionPrototype', - prototype: 'CompartmentPrototype', + '%InertCompartment%': { + '[[Proto]]': '%FunctionPrototype%', + prototype: '%CompartmentPrototype%', + toString: fn, }, - CompartmentPrototype: { - constructor: 'Compartment', + '%CompartmentPrototype%': { + constructor: '%InertCompartment%', evaluate: fn, globalThis: getter, import: asyncFn, importNow: fn, module: fn, + // Should this be proposed? + toString: fn, }, + lockdown: fn, + harden: fn, + StaticModuleRecord: { - '**proto**': 'FunctionPrototype', - prototype: 'StaticModuleRecordPrototype', + '[[Proto]]': '%FunctionPrototype%', + prototype: '%StaticModuleRecordPrototype%', + toString: fn, }, - StaticModuleRecordPrototype: { - constructor: 'StaticModuleRecord', + '%InertStaticModuleRecord%': { + '[[Proto]]': '%FunctionPrototype%', + prototype: '%StaticModuleRecordPrototype%', + toString: fn, }, - harden: fn, + '%StaticModuleRecordPrototype%': { + constructor: '%InertStaticModuleRecord%', + // Should this be proposed? + toString: fn, + }, + + '%InitialGetStackString%': fn, }; diff --git a/packages/ses/src/check-anon-intrinsics.js b/packages/ses/test/check-anon-intrinsics.js similarity index 72% rename from packages/ses/src/check-anon-intrinsics.js rename to packages/ses/test/check-anon-intrinsics.js index 810b40fbda..c34bf84d68 100644 --- a/packages/ses/src/check-anon-intrinsics.js +++ b/packages/ses/test/check-anon-intrinsics.js @@ -9,22 +9,26 @@ import { getPrototypeOf } from './commons.js'; export function checkAnonIntrinsics(intrinsics) { const { - FunctionPrototypeConstructor, - ArrayIteratorPrototype, - AsyncFunction, - AsyncGenerator, - AsyncGeneratorFunction, - AsyncGeneratorPrototype, - AsyncIteratorPrototype, - Generator, - GeneratorFunction, - IteratorPrototype, - MapIteratorPrototype, - RegExpStringIteratorPrototype, - SetIteratorPrototype, - StringIteratorPrototype, - ThrowTypeError, - TypedArray, + '%InertFunction%': InertFunction, + '%ArrayIteratorPrototype%': ArrayIteratorPrototype, + '%InertAsyncFunction%': AsyncFunction, + '%AsyncGenerator%': AsyncGenerator, + '%InertAsyncGeneratorFunction%': AsyncGeneratorFunction, + '%AsyncGeneratorPrototype%': AsyncGeneratorPrototype, + '%AsyncIteratorPrototype%': AsyncIteratorPrototype, + '%Generator%': Generator, + '%InertGeneratorFunction%': GeneratorFunction, + '%IteratorPrototype%': IteratorPrototype, + '%MapIteratorPrototype%': MapIteratorPrototype, + '%RegExpStringIteratorPrototype%': RegExpStringIteratorPrototype, + '%SetIteratorPrototype%': SetIteratorPrototype, + '%StringIteratorPrototype%': StringIteratorPrototype, + '%ThrowTypeError%': ThrowTypeError, + '%TypedArray%': TypedArray, + '%SharedDate%': SharedDate, + '%SharedError%': SharedError, + '%SharedRegExp%': SharedRegExp, + // '%SharedMath%': SharedMath, // Can't get to SharedMath by navigation } = intrinsics; // 9.2.4.1 %ThrowTypeError% @@ -98,7 +102,7 @@ export function checkAnonIntrinsics(intrinsics) { // Use Function.prototype.constructor in case Function has been tamed assert( - getPrototypeOf(GeneratorFunction) === FunctionPrototypeConstructor, + getPrototypeOf(GeneratorFunction) === InertFunction, 'GeneratorFunction.__proto__ should be Function', ); @@ -118,7 +122,7 @@ export function checkAnonIntrinsics(intrinsics) { // Use Function.prototype.constructor in case Function has been tamed assert( - getPrototypeOf(AsyncGeneratorFunction) === FunctionPrototypeConstructor, + getPrototypeOf(AsyncGeneratorFunction) === InertFunction, 'AsyncGeneratorFunction.__proto__ should be Function', ); assert( @@ -144,11 +148,21 @@ export function checkAnonIntrinsics(intrinsics) { // Use Function.prototype.constructor in case Function has been tamed assert( - getPrototypeOf(AsyncFunction) === FunctionPrototypeConstructor, + getPrototypeOf(AsyncFunction) === InertFunction, 'AsyncFunction.__proto__ should be Function', ); assert( AsyncFunction.name === 'AsyncFunction', 'AsyncFunction.name should be "AsyncFunction"', ); + assert(typeof SharedDate === 'function', `expected shared Date constructor`); + assert( + typeof SharedError === 'function', + `expected shared Error constructor`, + ); + assert( + typeof SharedRegExp === 'function', + `expected shared RegExp constructor`, + ); + // assert(typeof SharedMath === 'object', `expected shared Math object`); } diff --git a/packages/ses/src/check-intrinsics.js b/packages/ses/test/check-intrinsics.js similarity index 100% rename from packages/ses/src/check-intrinsics.js rename to packages/ses/test/check-intrinsics.js diff --git a/packages/ses/test/compartment-instance.test.js b/packages/ses/test/compartment-instance.test.js index 25977d004b..64dcf0e75d 100644 --- a/packages/ses/test/compartment-instance.test.js +++ b/packages/ses/test/compartment-instance.test.js @@ -15,7 +15,7 @@ test('Compartment instance', t => { t.equals(typeof c, 'object', 'typeof'); t.ok(c instanceof Compartment, 'instanceof'); - t.equals( + t.notEquals( c.constructor, Compartment, 'function Compartment() { [shim code] }', diff --git a/packages/ses/test/compartment-prototype.test.js b/packages/ses/test/compartment-prototype.test.js index 42f168f4f5..2ff17b0a4b 100644 --- a/packages/ses/test/compartment-prototype.test.js +++ b/packages/ses/test/compartment-prototype.test.js @@ -6,7 +6,7 @@ const { test } = tap; test('Compartment prototype', t => { t.plan(2); - t.equals( + t.notEquals( Compartment.prototype.constructor, Compartment, 'The initial value of Compartment.prototype.constructor', diff --git a/packages/ses/test/enable-property-overrides.test.js b/packages/ses/test/enable-property-overrides.test.js index fef57f0133..7af832f760 100644 --- a/packages/ses/test/enable-property-overrides.test.js +++ b/packages/ses/test/enable-property-overrides.test.js @@ -62,12 +62,12 @@ test('enablePropertyOverrides - on', t => { ); const intrinsics = { - ObjectPrototype: Object.prototype, - ArrayPrototype: Array.prototype, - FunctionPrototype: Function.prototype, - ErrorPrototype: Error.prototype, - TypeErrorPrototype: TypeError.prototype, - PromisePrototype: Promise.prototype, + '%ObjectPrototype%': Object.prototype, + '%ArrayPrototype%': Array.prototype, + '%FunctionPrototype%': Function.prototype, + '%ErrorPrototype%': Error.prototype, + '%TypeErrorPrototype%': TypeError.prototype, + '%PromisePrototype%': Promise.prototype, JSON, }; diff --git a/packages/ses/test/evaluate.test.js b/packages/ses/test/evaluate.test.js index 8be964cf6b..e97628e144 100644 --- a/packages/ses/test/evaluate.test.js +++ b/packages/ses/test/evaluate.test.js @@ -11,39 +11,38 @@ test('performEval - sloppyGlobalsMode', t => { // Mimic repairFunctions. stubFunctionConstructors(sinon); - const realmRec = { intrinsics: { eval: globalThis.eval, Function } }; // bypass esm const globalObject = {}; const endowments = { abc: 123 }; const options = { sloppyGlobalsMode: true }; t.equal( - performEval(realmRec, 'typeof def', globalObject, {}, options), + performEval('typeof def', globalObject, {}, options), 'undefined', 'typeof non declared global', ); t.equal( - performEval(realmRec, 'def', globalObject, {}, options), + performEval('def', globalObject, {}, options), undefined, 'non declared global do not cause a reference error', ); t.equal( - performEval(realmRec, 'abc', globalObject, endowments, options), + performEval('abc', globalObject, endowments, options), 123, 'endowments can be referenced', ); t.equal( - performEval(realmRec, 'abc', globalObject, {}, options), + performEval('abc', globalObject, {}, options), undefined, 'endowments do not persit', ); t.equal( - performEval(realmRec, 'def = abc + 333', globalObject, endowments, options), + performEval('def = abc + 333', globalObject, endowments, options), 456, 'define global', ); t.equal( - performEval(realmRec, 'def', globalObject, {}, options), + performEval('def', globalObject, {}, options), 456, 'defined global persists', ); @@ -58,7 +57,6 @@ test('performEval - transforms - rewrite source', t => { // Mimic repairFunctions. stubFunctionConstructors(sinon); - const realmRec = { intrinsics: { eval: globalThis.eval, Function } }; // bypass esm const globalObject = {}; const endowments = { abc: 123, def: 456 }; @@ -80,26 +78,24 @@ test('performEval - transforms - rewrite source', t => { }, ]; - t.equal( - performEval(realmRec, 'abc', globalObject, endowments), - 123, - 'no rewrite', - ); + t.equal(performEval('abc', globalObject, endowments), 123, 'no rewrite'); t.equal( - performEval(realmRec, 'ABC', globalObject, endowments, { + performEval('ABC', globalObject, endowments, { globalTransforms, }), 123, 'globalTransforms rewrite source', ); t.equal( - performEval(realmRec, 'ABC', globalObject, endowments, { localTransforms }), + performEval('ABC', globalObject, endowments, { + localTransforms, + }), 456, 'localTransforms rewrite source', ); t.equal( - performEval(realmRec, 'ABC', globalObject, endowments, { + performEval('ABC', globalObject, endowments, { localTransforms, globalTransforms, }), diff --git a/packages/ses/test/get-global-intrinsics.test.js b/packages/ses/test/get-global-intrinsics.test.js index 3484958ebe..064a7f219c 100644 --- a/packages/ses/test/get-global-intrinsics.test.js +++ b/packages/ses/test/get-global-intrinsics.test.js @@ -1,10 +1,11 @@ import tap from 'tap'; -import sinon from 'sinon'; -import { getGlobalIntrinsics } from '../src/intrinsics-global.js'; +// import sinon from 'sinon'; +// import { getGlobalIntrinsics } from '../src/intrinsics.js'; const { test } = tap; test('getGlobalIntrinsics', t => { + /* TODO // We to duplicate this structure here as an // as an independent refrerence. const globalNames = [ @@ -127,3 +128,6 @@ test('Intrinsics - global accessor throws', t => { sinon.restore(); }); +*/ + t.end(); +}); diff --git a/packages/ses/test/get-intrinsics.test.js b/packages/ses/test/get-intrinsics.test.js deleted file mode 100644 index ec56f33134..0000000000 --- a/packages/ses/test/get-intrinsics.test.js +++ /dev/null @@ -1,117 +0,0 @@ -import tap from 'tap'; -import { getIntrinsics } from '../src/intrinsics.js'; - -const { test } = tap; - -function ObjectHasOwnProperty(obj, prop) { - return Object.prototype.hasOwnProperty.call(obj, prop); -} - -// TODO Module -// eslint-disable-next-line no-eval -if (!eval.toString().includes('native code')) { - throw new TypeError('Module "esm" enabled: aborting'); -} - -const { getPrototypeOf } = Object; - -function getAnonIntrinsics() { - const FunctionPrototypeConstructor = Function.prototype.constructor; - - // eslint-disable-next-line no-new-wrappers - const StringIteratorObject = new String()[Symbol.iterator](); - const StringIteratorPrototype = getPrototypeOf(StringIteratorObject); - - const RegExpStringIteratorObject = new RegExp()[Symbol.matchAll](); - const RegExpStringIteratorPrototype = getPrototypeOf( - RegExpStringIteratorObject, - ); - - // eslint-disable-next-line no-array-constructor - const ArrayIteratorInstance = new Array()[Symbol.iterator](); - const ArrayIteratorPrototype = getPrototypeOf(ArrayIteratorInstance); - - const MapIteratorObject = new Map()[Symbol.iterator](); - const MapIteratorPrototype = getPrototypeOf(MapIteratorObject); - - const SetIteratorObject = new Set()[Symbol.iterator](); - const SetIteratorPrototype = getPrototypeOf(SetIteratorObject); - - const IteratorPrototype = getPrototypeOf(ArrayIteratorPrototype); - - const TypedArray = getPrototypeOf(Int8Array); - - // eslint-disable-next-line func-names, no-empty-function - const AsyncFunctionInstance = async function() {}; - const AsyncFunction = getPrototypeOf(AsyncFunctionInstance).constructor; - - // eslint-disable-next-line func-names, no-empty-function - const GeneratorFunctionInstance = function*() {}; - const GeneratorFunction = getPrototypeOf(GeneratorFunctionInstance) - .constructor; - - const Generator = GeneratorFunction.prototype; - - // eslint-disable-next-line func-names, no-empty-function - const AsyncGeneratorFunctionInstance = async function*() {}; - const AsyncGeneratorFunction = getPrototypeOf(AsyncGeneratorFunctionInstance) - .constructor; - - const AsyncGenerator = AsyncGeneratorFunction.prototype; - - const AsyncGeneratorPrototype = AsyncGenerator.prototype; - - const AsyncIteratorPrototype = getPrototypeOf(AsyncGeneratorPrototype); - - // eslint-disable-next-line func-names - const ThrowTypeError = (function() { - // eslint-disable-next-line prefer-rest-params - return Object.getOwnPropertyDescriptor(arguments, 'callee').get; - })(); - - return { - FunctionPrototypeConstructor, - StringIteratorPrototype, - RegExpStringIteratorPrototype, - ArrayIteratorPrototype, - MapIteratorPrototype, - SetIteratorPrototype, - IteratorPrototype, - TypedArray, - AsyncFunction, - GeneratorFunction, - Generator, - AsyncGeneratorFunction, - AsyncGenerator, - AsyncGeneratorPrototype, - AsyncIteratorPrototype, - ThrowTypeError, - }; -} - -test('intrinsics - getIntrinsics', t => { - const instrinsics = getIntrinsics(); - - const anonIntrinsics = getAnonIntrinsics(); - - for (const name of Object.keys(instrinsics)) { - if (ObjectHasOwnProperty(anonIntrinsics, name)) { - t.equal(instrinsics[name], anonIntrinsics[name], name); - } else if (ObjectHasOwnProperty(globalThis, name)) { - t.equal(instrinsics[name], globalThis[name], name); - } else if (name.endsWith('Prototype')) { - const base = name.slice(0, -9); - if (ObjectHasOwnProperty(anonIntrinsics, base)) { - t.equal(instrinsics[name], anonIntrinsics[base].prototype, name); - } else if (ObjectHasOwnProperty(globalThis, base)) { - t.equal(instrinsics[name], globalThis[base].prototype, name); - } else { - t.skip(name); - } - } else { - t.skip(name); - } - } - - t.end(); -}); diff --git a/packages/ses/test/global-object-properties.test.js b/packages/ses/test/global-object-properties.test.js index 38825f7d8c..f654460501 100644 --- a/packages/ses/test/global-object-properties.test.js +++ b/packages/ses/test/global-object-properties.test.js @@ -29,7 +29,7 @@ test('globalObject properties', t => { t.notEqual(c.globalThis.Function, Function); t.notEqual(c.globalThis.Function, globalThis.Function); - t.equal(c.globalThis.Compartment, Compartment); + t.notEqual(c.globalThis.Compartment, Compartment); delete globalThis.Compartment; sinon.restore(); diff --git a/packages/ses/test/global-object.test.js b/packages/ses/test/global-object.test.js index e71ef2a367..cb5081ba76 100644 --- a/packages/ses/test/global-object.test.js +++ b/packages/ses/test/global-object.test.js @@ -1,26 +1,24 @@ import tap from 'tap'; import sinon from 'sinon'; -import { createGlobalObject } from '../src/global-object.js'; +import { initGlobalObject } from '../src/global-object.js'; import stubFunctionConstructors from './stub-function-constructors.js'; +import { sharedGlobalPropertyNames } from '../src/whitelist.js'; const { test } = tap; test('globalObject', t => { - t.plan(38); - // Mimic repairFunctions. stubFunctionConstructors(sinon); - const realmRec = { - intrinsics: { - Date: {}, - eval: globalThis.eval, - Function: globalThis.Function, - globalThis: {}, - }, + const intrinsics = { + Date: globalThis.Date, + eval: globalThis.eval, + Function: globalThis.Function, + globalThis: {}, }; - const globalObject = createGlobalObject(realmRec, {}); + const globalObject = {}; + initGlobalObject(globalObject, intrinsics, sharedGlobalPropertyNames, {}); t.ok(globalObject instanceof Object); t.equal(Object.getPrototypeOf(globalObject), Object.prototype); @@ -43,7 +41,7 @@ test('globalObject', t => { } else if (['eval', 'Function', 'globalThis'].includes(name)) { t.notEqual( desc.value, - realmRec.intrinsics[name], + intrinsics[name], `${name} should not be the intrinsics ${name}`, ); t.notEqual( @@ -51,17 +49,6 @@ test('globalObject', t => { globalThis[name], `${name} should not be the global ${name}`, ); - } else { - t.equal( - desc.value, - realmRec.intrinsics[name], - `${name} should be the intrinsics ${name}`, - ); - t.notEqual( - desc.value, - globalThis[name], - `${name} should not be the global ${name}`, - ); } if (['Infinity', 'NaN', 'undefined'].includes(name)) { @@ -76,4 +63,6 @@ test('globalObject', t => { } sinon.restore(); + + t.end(); }); diff --git a/packages/ses/test/identity-continuity.test.js b/packages/ses/test/identity-continuity.test.js index 90d558f26b..86a41db97b 100644 --- a/packages/ses/test/identity-continuity.test.js +++ b/packages/ses/test/identity-continuity.test.js @@ -33,7 +33,7 @@ test('identity Array', t => { // Compartment is a shared global test('identity Compartment', t => { - t.plan(7); + t.plan(8); // Mimic repairFunctions. stubFunctionConstructors(sinon); @@ -43,10 +43,14 @@ test('identity Compartment', t => { const c1 = new Compartment(); const c2 = new c1.globalThis.Compartment(); - t.equal(c1.evaluate('Compartment'), Compartment); + t.notEqual(c1.evaluate('Compartment'), Compartment); t.equal(c1.evaluate('Compartment'), c1.evaluate('Compartment')); - t.equal(c1.evaluate('Compartment'), c2.evaluate('Compartment')); - t.equal(c1.evaluate('Compartment'), c2.evaluate('(0,eval)("Compartment")')); + t.notEqual(c1.evaluate('Compartment'), c2.evaluate('Compartment')); + t.equal(c1.evaluate('Compartment'), c1.evaluate('(0,eval)("Compartment")')); + t.notEqual( + c1.evaluate('Compartment'), + c2.evaluate('(0,eval)("Compartment")'), + ); const e3 = c2.evaluate('(new Compartment())'); t.ok(e3 instanceof Compartment); @@ -55,6 +59,7 @@ test('identity Compartment', t => { delete globalThis.Compartment; sinon.restore(); + t.end(); }); // eval is evaluator-specific diff --git a/packages/ses/test/lockdown-allow.test.js b/packages/ses/test/lockdown-allow.test.js index 7312420c4f..b5cfb25bb8 100644 --- a/packages/ses/test/lockdown-allow.test.js +++ b/packages/ses/test/lockdown-allow.test.js @@ -1,5 +1,6 @@ +/* global lockdown */ import test from 'tape'; -import { lockdown } from '../src/lockdown-shim.js'; +import '../src/main.js'; test('lockdown returns boolean or throws in downgraded SES', t => { t.plan(6); diff --git a/packages/ses/test/lockdown.test.js b/packages/ses/test/lockdown.test.js index 093e49da75..023a0e2898 100644 --- a/packages/ses/test/lockdown.test.js +++ b/packages/ses/test/lockdown.test.js @@ -1,6 +1,6 @@ +/* global Compartment, lockdown */ import test from 'tape'; -import { Compartment } from '../src/compartment-shim.js'; -import { lockdown } from '../src/lockdown-shim.js'; +import '../src/main.js'; test('lockdown returns boolean or throws in SES', t => { // Compartment constructor does not throw before lockdown. diff --git a/packages/ses/test/make-eval-function.test.js b/packages/ses/test/make-eval-function.test.js index b64560b5dc..05e9912881 100644 --- a/packages/ses/test/make-eval-function.test.js +++ b/packages/ses/test/make-eval-function.test.js @@ -11,9 +11,8 @@ test('makeEvalFunction - leak', t => { // Mimic repairFunctions. stubFunctionConstructors(sinon); - const realmRec = { intrinsics: { eval: globalThis.eval, Function } }; // bypass esm const globalObject = {}; - const safeEval = makeEvalFunction(realmRec, globalObject); + const safeEval = makeEvalFunction(globalObject); t.throws(() => safeEval('none'), ReferenceError); t.equal(safeEval('this.none'), undefined); @@ -44,7 +43,6 @@ test('makeEvalFunction - globals', t => { // Mimic repairFunctions. stubFunctionConstructors(sinon); - const realmRec = { intrinsics: { eval: globalThis.eval, Function } }; // bypass esm const globalObject = Object.create( {}, { @@ -52,7 +50,7 @@ test('makeEvalFunction - globals', t => { bar: { value: 2, writable: true }, }, ); - const safeEval = makeEvalFunction(realmRec, globalObject); + const safeEval = makeEvalFunction(globalObject); t.equal(safeEval('foo'), 1); t.equal(safeEval('bar'), 2); diff --git a/packages/ses/test/make-evaluate-factory.test.js b/packages/ses/test/make-evaluate-factory.test.js index 1c3d9471e5..917fc88c01 100644 --- a/packages/ses/test/make-evaluate-factory.test.js +++ b/packages/ses/test/make-evaluate-factory.test.js @@ -6,14 +6,13 @@ const { test } = tap; test('Intrinsics - values', t => { t.plan(2); - const realmRec = { intrinsics: { Function } }; t.equals( - makeEvaluateFactory(realmRec).toString(), + makeEvaluateFactory().toString(), "function anonymous(\n) {\n\n with (this) {\n \n return function() {\n 'use strict';\n return eval(arguments[0]);\n };\n }\n \n}", ); t.equals( - makeEvaluateFactory(realmRec, ['foot']).toString(), + makeEvaluateFactory(['foot']).toString(), "function anonymous(\n) {\n\n with (this) {\n const {foot} = this;\n return function() {\n 'use strict';\n return eval(arguments[0]);\n };\n }\n \n}", ); }); diff --git a/packages/ses/test/make-function-constructor.test.js b/packages/ses/test/make-function-constructor.test.js index 99bdc8f66c..71eb159e87 100644 --- a/packages/ses/test/make-function-constructor.test.js +++ b/packages/ses/test/make-function-constructor.test.js @@ -11,7 +11,6 @@ test('functionConstructor', t => { // Mimic repairFunctions. stubFunctionConstructors(sinon); - const realmRec = { intrinsics: { eval: globalThis.eval, Function } }; // bypass esm const globalObject = Object.create( {}, { @@ -19,7 +18,7 @@ test('functionConstructor', t => { bar: { value: 2, writable: true }, }, ); - const safeFunction = makeFunctionConstructor(realmRec, globalObject); + const safeFunction = makeFunctionConstructor(globalObject); t.equal(safeFunction('return foo')(), 1); t.equal(safeFunction('return bar')(), 2); diff --git a/packages/ses/test/realm-rec.test.js b/packages/ses/test/realm-rec.test.js deleted file mode 100644 index f19f82d3c0..0000000000 --- a/packages/ses/test/realm-rec.test.js +++ /dev/null @@ -1,27 +0,0 @@ -import tap from 'tap'; -import { getGlobalIntrinsics } from '../src/intrinsics-global.js'; -import { getCurrentRealmRec } from '../src/realm-rec.js'; - -const { test } = tap; - -test('realmRec', t => { - t.plan(4); - - // TODO mock realmRec module dependencies instead of mimicking them. - const realmRec = getCurrentRealmRec(); - const intrinsics = getGlobalIntrinsics(); - - t.equal(Object.getPrototypeOf(realmRec), null); - t.ok(Object.isFrozen(realmRec)); - t.deepEqual(realmRec.intrinsics, intrinsics); - - const descs = Object.getOwnPropertyDescriptors(realmRec); - t.deepEqual(descs, { - intrinsics: { - value: intrinsics, - configurable: false, - writable: false, - enumerable: true, // not important, implementation-specific - }, - }); -}); diff --git a/packages/ses/test/scope-handler.test.js b/packages/ses/test/scope-handler.test.js index c7b9d85874..902663c897 100644 --- a/packages/ses/test/scope-handler.test.js +++ b/packages/ses/test/scope-handler.test.js @@ -4,15 +4,20 @@ import { createScopeHandler } from '../src/scope-handler.js'; const { test } = tap; +// The original unsafe untamed eval function, which must not escape. +// Sample at module initialization time, which is before lockdown can +// repair it. Use it only to build powerless abstractions. +// eslint-disable-next-line no-eval +const FERAL_EVAL = eval; + test('scopeHandler - has trap', t => { t.plan(7); globalThis.bar = {}; - const realmRec = { intrinsics: { Function } }; const globalObject = { foo: {} }; const endowments = { foobar: {} }; - const handler = createScopeHandler(realmRec, globalObject, endowments); + const handler = createScopeHandler(globalObject, endowments); t.equal(handler.has(null, Symbol.unscopables), false); t.equal(handler.has(null, 'arguments'), false); @@ -29,16 +34,10 @@ test('scopeHandler - has trap', t => { test('scopeHandler - has trap in sloppyGlobalsMode', t => { t.plan(7); - const realmRec = { intrinsics: { Function } }; const globalObject = {}; const endowments = {}; const options = { sloppyGlobalsMode: true }; - const handler = createScopeHandler( - realmRec, - globalObject, - endowments, - options, - ); + const handler = createScopeHandler(globalObject, endowments, options); globalThis.bar = {}; @@ -57,10 +56,9 @@ test('scopeHandler - has trap in sloppyGlobalsMode', t => { test('scopeHandler - get trap', t => { t.plan(7); - const realmRec = { intrinsics: { eval: globalThis.eval, Function } }; // bypass esm const globalObject = { foo: {} }; const endowments = { foobar: {} }; - const handler = createScopeHandler(realmRec, globalObject, endowments); + const handler = createScopeHandler(globalObject, endowments); globalThis.bar = {}; @@ -79,10 +77,9 @@ test('scopeHandler - get trap', t => { test('scopeHandler - get trap - accessors on endowments', t => { t.plan(2); - const realmRec = { intrinsics: { Function } }; const globalObject = { foo: {} }; const endowments = {}; - const handler = createScopeHandler(realmRec, globalObject, endowments); + const handler = createScopeHandler(globalObject, endowments); Object.defineProperties(endowments, { foo: { @@ -99,10 +96,9 @@ test('scopeHandler - get trap - accessors on endowments', t => { test('scopeHandler - set trap', t => { t.plan(13); - const realmRec = { intrinsics: { Function } }; const globalObject = { foo: {} }; const endowments = { foobar: {} }; - const handler = createScopeHandler(realmRec, globalObject, endowments); + const handler = createScopeHandler(globalObject, endowments); globalThis.bar = {}; @@ -142,16 +138,15 @@ test('scopeHandler - set trap', t => { test('scopeHandler - get trap - useUnsafeEvaluator', t => { t.plan(7); - const realmRec = { intrinsics: { eval: globalThis.eval, Function } }; // bypass esm const globalObject = { eval: {} }; - const handler = createScopeHandler(realmRec, globalObject); + const handler = createScopeHandler(globalObject); t.equal(handler.useUnsafeEvaluator, false); t.equal(handler.get(null, 'eval'), globalObject.eval); t.equal(handler.get(null, 'eval'), globalObject.eval); // repeat handler.useUnsafeEvaluator = true; - t.equal(handler.get(null, 'eval'), realmRec.intrinsics.eval); + t.equal(handler.get(null, 'eval'), FERAL_EVAL); t.equal(handler.useUnsafeEvaluator, false); t.equal(handler.get(null, 'eval'), globalObject.eval); t.equal(handler.get(null, 'eval'), globalObject.eval); // repeat @@ -162,9 +157,8 @@ test('scopeHandler - throw only for unsupported traps', t => { sinon.stub(console, 'error').callsFake(); - const realmRec = { intrinsics: { Function } }; const globalObject = {}; - const handler = createScopeHandler(realmRec, globalObject); + const handler = createScopeHandler(globalObject); ['has', 'get', 'set', 'getPrototypeOf'].forEach(trap => t.doesNotThrow(() => handler[trap]), diff --git a/packages/ses/test/static-module-record-unit.test.js b/packages/ses/test/static-module-record-unit.test.js new file mode 100644 index 0000000000..1184bd76e4 --- /dev/null +++ b/packages/ses/test/static-module-record-unit.test.js @@ -0,0 +1,29 @@ +import tap from 'tap'; +import { StaticModuleRecord } from '../src/compartment-shim.js'; + +const { test } = tap; + +test('static module record constructor', t => { + t.plan(1); + + const smr = new StaticModuleRecord('export default 10'); + t.same(smr.imports, []); +}); + +test('static module record constructor without new', t => { + t.plan(1); + + const smr = StaticModuleRecord('export default 10'); + t.same(smr.imports, []); +}); + +test('module imports list', t => { + t.plan(1); + + const smr = new StaticModuleRecord(` + import namedExport from 'namedModule'; + import otherName from 'namedModule'; + import yetAnother from 'otherModule'; + `); + t.same(smr.imports, ['namedModule', 'otherModule']); +}); diff --git a/packages/ses/test/tame-date-allow.test.js b/packages/ses/test/tame-date-allow.test.js deleted file mode 100644 index cc396d634d..0000000000 --- a/packages/ses/test/tame-date-allow.test.js +++ /dev/null @@ -1,43 +0,0 @@ -/* global Compartment, lockdown */ -import test from 'tape'; -import '../src/main.js'; - -lockdown({ dateTaming: 'unsafe' }); - -function isDate(date) { - return ( - Object.prototype.toString.call(date) === '[object Date]' && - Number.isInteger(date.getTime()) - ); -} - -test('lockdown() date allowed - Date in Compartment is not tamed', t => { - const start = Date.now(); - - const c = new Compartment(); - t.equal(c.evaluate('Date.parse("1982-04-09")'), Date.parse('1982-04-09')); - - const now = c.evaluate('Date.now()'); - t.assert(Number.isInteger(now)); - - const finished = Date.now(); - t.ok(start <= now); - t.ok(now <= finished); - - const newDate = c.evaluate('new Date()'); - t.ok(isDate(newDate)); - - t.end(); -}); - -test('lockdown() date allowed - Date in nested Compartment is not tamed', t => { - const c = new Compartment().evaluate('new Compartment()'); - - const now = c.evaluate('Date.now()'); - t.equal(Number.isNaN(now), false); - - const newDate = c.evaluate('new Date()'); - t.ok(isDate(newDate)); - - t.end(); -}); diff --git a/packages/ses/test/tame-date-unit.test.js b/packages/ses/test/tame-date-unit.test.js new file mode 100644 index 0000000000..0f36504da1 --- /dev/null +++ b/packages/ses/test/tame-date-unit.test.js @@ -0,0 +1,57 @@ +import tap from 'tap'; +import tameDateConstructor from '../src/tame-date-constructor.js'; + +const { test } = tap; + +const { + '%InitialDate%': InitialDate, + '%SharedDate%': SharedDate, +} = tameDateConstructor(); + +test('tameDateConstructor - initial constructor without argument', t => { + t.equal(InitialDate.name, 'Date'); + + const date = new InitialDate(); + + t.ok(date instanceof InitialDate); + // eslint-disable-next-line no-proto + t.equal(date.__proto__.constructor, SharedDate); + + t.isNot(date.toString(), 'Invalid Date'); + + t.end(); +}); + +test('tameDateConstructor - shared constructor without argument', t => { + t.equal(SharedDate.name, 'Date'); + + const date = new SharedDate(); + + t.ok(date instanceof SharedDate); + // eslint-disable-next-line no-proto + t.equal(date.__proto__.constructor, SharedDate); + + t.equal(date.toString(), 'Invalid Date'); + + t.end(); +}); + +test('tameDateConstructor - shared constructor now', t => { + t.equal(SharedDate.now.name, 'now'); + + const date = SharedDate.now(); + + t.ok(Number.isNaN(date)); + + t.end(); +}); + +test('tameDateConstructor - initial constructor now', t => { + t.equal(InitialDate.now.name, 'now'); + + const date = InitialDate.now(); + + t.ok(date > 1); + + t.end(); +}); diff --git a/packages/ses/test/tame-date.test.js b/packages/ses/test/tame-date.test.js index 10f709318b..6deb9ca303 100644 --- a/packages/ses/test/tame-date.test.js +++ b/packages/ses/test/tame-date.test.js @@ -4,12 +4,35 @@ import '../src/main.js'; lockdown(); -test('lockdown() default - Date in Compartment is tamed', t => { +function isDate(date) { + return ( + Object.prototype.toString.call(date) === '[object Date]' && + Number.isInteger(date.getTime()) + ); +} + +test('lockdown start Date is powerful', t => { + t.ok(Number.isInteger(Date.now())); + t.ok(isDate(new Date())); + + t.end(); +}); + +test('lockdown Date.prototype.constructor is powerless', t => { + const SharedDate = Date.prototype.constructor; + t.notEqual(Date, SharedDate); + t.ok(Number.isNaN(SharedDate.now())); + t.equal(`${new SharedDate()}`, 'Invalid Date'); + + t.end(); +}); + +test('lockdown Date in Compartment is powerless', t => { const c = new Compartment(); t.equal(c.evaluate('Date.parse("1982-04-09")'), Date.parse('1982-04-09')); const now = c.evaluate('Date.now()'); - t.equal(Number.isNaN(now), true); + t.ok(Number.isNaN(now)); const newDate = c.evaluate('new Date()'); t.equal(`${newDate}`, 'Invalid Date'); @@ -17,11 +40,11 @@ test('lockdown() default - Date in Compartment is tamed', t => { t.end(); }); -test('lockdown() default - Date in nested Compartment is tamed', t => { +test('lockdown Date in nested Compartment is powerless', t => { const c = new Compartment().evaluate('new Compartment()'); const now = c.evaluate('Date.now()'); - t.equal(Number.isNaN(now), true); + t.ok(Number.isNaN(now)); const newDate = c.evaluate('new Date()'); t.equal(`${newDate}`, 'Invalid Date'); diff --git a/packages/ses/test/tame-error-allow.test.js b/packages/ses/test/tame-error-allow.test.js deleted file mode 100644 index 52e7c73675..0000000000 --- a/packages/ses/test/tame-error-allow.test.js +++ /dev/null @@ -1,28 +0,0 @@ -/* global Compartment, lockdown */ -import test from 'tape'; -import '../src/main.js'; - -lockdown({ errorTaming: 'unsafe' }); - -test('lockdown allow Error - Error is tamed unsafe', t => { - t.equal(typeof Error.captureStackTrace, 'function'); - t.equal(typeof Error.stackTraceLimit, 'number'); - t.equal(typeof new Error().stack, 'string'); - t.end(); -}); - -test('lockdown allow Error - Error in Compartment is tamed unsafe', t => { - const c = new Compartment(); - t.equal(c.evaluate('typeof Error.captureStackTrace'), 'function'); - t.equal(c.evaluate('typeof Error.stackTraceLimit'), 'number'); - t.equal(c.evaluate('typeof new Error().stack'), 'string'); - t.end(); -}); - -test('lockdown allow Error - Error in nested Compartment is tamed unsafe', t => { - const c = new Compartment().evaluate('new Compartment()'); - t.equal(c.evaluate('typeof Error.captureStackTrace'), 'function'); - t.equal(c.evaluate('typeof Error.stackTraceLimit'), 'number'); - t.equal(c.evaluate('typeof new Error().stack'), 'string'); - t.end(); -}); diff --git a/packages/ses/test/tame-error.test.js b/packages/ses/test/tame-error.test.js deleted file mode 100644 index 38a6c1c9b7..0000000000 --- a/packages/ses/test/tame-error.test.js +++ /dev/null @@ -1,28 +0,0 @@ -/* global Compartment, lockdown */ -import test from 'tape'; -import '../src/main.js'; - -lockdown(); - -test('lockdown default - Error is safe', t => { - t.equal(typeof Error.captureStackTrace, 'function'); - t.equal(Error.stackTraceLimit, undefined); - t.equal(typeof new Error().stack, 'string'); - t.end(); -}); - -test('lockdown default - Error in Compartment is safe', t => { - const c = new Compartment(); - t.equal(c.evaluate('typeof Error.captureStackTrace'), 'function'); - t.equal(c.evaluate('Error.stackTraceLimit'), undefined); - t.equal(c.evaluate('typeof new Error().stack'), 'string'); - t.end(); -}); - -test('lockdown default - Error in nested Compartment is safe', t => { - const c = new Compartment().evaluate('new Compartment()'); - t.equal(c.evaluate('typeof Error.captureStackTrace'), 'function'); - t.equal(c.evaluate('Error.stackTraceLimit'), undefined); - t.equal(c.evaluate('typeof new Error().stack'), 'string'); - t.end(); -}); diff --git a/packages/ses/test/tame-function-constructors.test.js b/packages/ses/test/tame-function-unit.test.js similarity index 100% rename from packages/ses/test/tame-function-constructors.test.js rename to packages/ses/test/tame-function-unit.test.js diff --git a/packages/ses/test/tame-global-date-object-allow.test.js b/packages/ses/test/tame-global-date-object-allow.test.js deleted file mode 100644 index e192fe04a6..0000000000 --- a/packages/ses/test/tame-global-date-object-allow.test.js +++ /dev/null @@ -1,38 +0,0 @@ -import tap from 'tap'; -import tameGlobalDateObject from '../src/tame-global-date-object.js'; - -const { test } = tap; - -const { - start: { - Date: { value: tamedDate }, - }, - shared: { - Date: { value: sharedDate }, - }, -} = tameGlobalDateObject('unsafe'); - -test('tameGlobalDateObject - constructor without argument', t => { - t.equal(tamedDate.name, 'Date'); - - // eslint-disable-next-line new-cap - const date = new tamedDate(); - - t.ok(date instanceof tamedDate); - // eslint-disable-next-line no-proto - t.equal(date.__proto__.constructor, sharedDate); - - t.isNot(date.toString(), 'Invalid Date'); - - t.end(); -}); - -test('tameGlobalDateObject - now', t => { - t.equal(tamedDate.now.name, 'now'); - - const date = tamedDate.now(); - - t.ok(date > 1); - - t.end(); -}); diff --git a/packages/ses/test/tame-global-date-object.test.js b/packages/ses/test/tame-global-date-object.test.js deleted file mode 100644 index e7a68cc1ba..0000000000 --- a/packages/ses/test/tame-global-date-object.test.js +++ /dev/null @@ -1,38 +0,0 @@ -import tap from 'tap'; -import tameGlobalDateObject from '../src/tame-global-date-object.js'; - -const { test } = tap; - -const { - start: { - Date: { value: tamedDate }, - }, - shared: { - Date: { value: sharedDate }, - }, -} = tameGlobalDateObject('safe'); - -test('tameGlobalDateObject - constructor without argument', t => { - t.equal(tamedDate.name, 'Date'); - - // eslint-disable-next-line new-cap - const date = new tamedDate(); - - t.ok(date instanceof tamedDate); - // eslint-disable-next-line no-proto - t.equal(date.__proto__.constructor, sharedDate); - - t.equal(date.toString(), 'Invalid Date'); - - t.end(); -}); - -test('tameGlobalDateObject - now', t => { - t.equal(tamedDate.now.name, 'now'); - - const date = tamedDate.now(); - - t.ok(Number.isNaN(date)); - - t.end(); -}); diff --git a/packages/ses/test/tame-global-error-object-allow.test.js b/packages/ses/test/tame-global-error-object-allow.test.js deleted file mode 100644 index 6755e98407..0000000000 --- a/packages/ses/test/tame-global-error-object-allow.test.js +++ /dev/null @@ -1,27 +0,0 @@ -import tap from 'tap'; -import tameGlobalErrorObject from '../src/tame-global-error-object.js'; - -const { test } = tap; - -const { - start: { - Error: { value: tamedError }, - }, -} = tameGlobalErrorObject('unsafe'); - -test('tameGlobalErrorObject', t => { - try { - t.equal(typeof tamedError.stackTraceLimit, 'number'); - tamedError.stackTraceLimit = 11; - t.equal(tamedError.stackTraceLimit, 11); - // eslint-disable-next-line new-cap - const error = new tamedError(); - t.equal(typeof error.stack, 'string'); - tamedError.captureStackTrace(error); - t.equal(typeof error.stack, 'string'); - } catch (e) { - t.isNot(e, e, 'unexpected exception'); - } finally { - t.end(); - } -}); diff --git a/packages/ses/test/tame-global-error-object.test.js b/packages/ses/test/tame-global-error-object.test.js deleted file mode 100644 index 6119963d18..0000000000 --- a/packages/ses/test/tame-global-error-object.test.js +++ /dev/null @@ -1,27 +0,0 @@ -import tap from 'tap'; -import tameGlobalErrorObject from '../src/tame-global-error-object.js'; - -const { test } = tap; - -const { - start: { - Error: { value: tamedError }, - }, -} = tameGlobalErrorObject('safe'); - -test('tameGlobalErrorObject', t => { - try { - t.equal(tamedError.stackTraceLimit, undefined); - tamedError.stackTraceLimit = 11; - t.equal(tamedError.stackTraceLimit, undefined); - // eslint-disable-next-line new-cap - const error = new tamedError(); - t.equal(typeof error.stack, 'string'); - tamedError.captureStackTrace(error); - t.equal(error.stack, ''); - } catch (e) { - t.isNot(e, e, 'unexpected exception'); - } finally { - t.end(); - } -}); diff --git a/packages/ses/test/tame-global-math-object-allow.test.js b/packages/ses/test/tame-global-math-object-allow.test.js deleted file mode 100644 index ca9f41d878..0000000000 --- a/packages/ses/test/tame-global-math-object-allow.test.js +++ /dev/null @@ -1,18 +0,0 @@ -import tap from 'tap'; -import tameGlobalMathObject from '../src/tame-global-math-object.js'; - -const { test } = tap; - -const { - start: { - Math: { value: tamedMath }, - }, -} = tameGlobalMathObject('unsafe'); - -test('tameGlobalMathObject - tamed properties', t => { - t.equal(tamedMath.random.name, 'random'); - - t.equal(typeof tamedMath.random(), 'number'); - - t.end(); -}); diff --git a/packages/ses/test/tame-global-math-object.test.js b/packages/ses/test/tame-global-math-object.test.js deleted file mode 100644 index fdfe28defd..0000000000 --- a/packages/ses/test/tame-global-math-object.test.js +++ /dev/null @@ -1,18 +0,0 @@ -import tap from 'tap'; -import tameGlobalMathObject from '../src/tame-global-math-object.js'; - -const { test } = tap; - -const { - start: { - Math: { value: tamedMath }, - }, -} = tameGlobalMathObject('safe'); - -test('tameGlobalMathObject - tamed properties', t => { - t.equal(tamedMath.random.name, 'random'); - - t.throws(() => tamedMath.random(), TypeError); - - t.end(); -}); diff --git a/packages/ses/test/tame-global-reg-exp-object-allow.test.js b/packages/ses/test/tame-global-reg-exp-object-allow.test.js deleted file mode 100644 index f63762a165..0000000000 --- a/packages/ses/test/tame-global-reg-exp-object-allow.test.js +++ /dev/null @@ -1,72 +0,0 @@ -import tap from 'tap'; -import tameGlobalRegExpObject from '../src/tame-global-reg-exp-object.js'; - -const { test } = tap; - -const { - start: { - RegExp: { value: tamedRegExp }, - }, - shared: { - RegExp: { value: sharedRegExp }, - }, -} = tameGlobalRegExpObject('unsafe'); - -test('tameGlobalRegExpObject - unsafeRegExp denied', t => { - const regexp = /./; - t.ok(regexp.constructor === sharedRegExp, 'tamed constructor not reached'); - - t.end(); -}); - -test('tameGlobalRegExpObject - undeniable prototype', t => { - // Don't try to deny the undeniable - // https://github.com/Agoric/SES-shim/issues/237 - // eslint-disable-next-line new-cap - const regexp1 = new tamedRegExp('.'); - const regexp2 = tamedRegExp('.'); - const regexp3 = /./; - t.ok( - // eslint-disable-next-line no-proto - regexp1.__proto__ === regexp2.__proto__, - 'new vs non-new instances differ', - ); - t.ok( - // eslint-disable-next-line no-proto - regexp1.__proto__ === regexp3.__proto__, - 'new vs literal instances differ', - ); - - t.ok( - regexp1 instanceof tamedRegExp, - 'new instance not instanceof tamed constructor', - ); - t.ok( - regexp2 instanceof tamedRegExp, - 'non-new instance not instanceof tamed constructor', - ); - t.ok( - regexp3 instanceof tamedRegExp, - 'literal instance not instanceof tamed constructor', - ); - - t.end(); -}); - -test('tameGlobalRegExpObject - constructor', t => { - t.equal(tamedRegExp.name, 'RegExp'); - t.equal(tamedRegExp.prototype.constructor, sharedRegExp); - - // eslint-disable-next-line new-cap - const regexp = new tamedRegExp(); - t.ok(regexp instanceof tamedRegExp); - // eslint-disable-next-line no-proto - t.equal(regexp.__proto__.constructor, sharedRegExp); - - // bare tamedRegExp() (without 'new') was failing - // https://github.com/Agoric/SES-shim/issues/230 - t.equal(tamedRegExp('foo').test('bar'), false); - t.equal(tamedRegExp('foo').test('foobar'), true); - - t.end(); -}); diff --git a/packages/ses/test/tame-locale-methods-allow.test.js b/packages/ses/test/tame-locale-methods-allow.test.js deleted file mode 100644 index 86aaab8557..0000000000 --- a/packages/ses/test/tame-locale-methods-allow.test.js +++ /dev/null @@ -1,25 +0,0 @@ -/* global lockdown BigInt */ -import test from 'tape'; -import '../src/main.js'; - -lockdown(); - -test('tame locale methods', t => { - t.equal(Object.prototype.toString, Object.prototype.toLocaleString); - t.equal(Number.prototype.toString, Number.prototype.toLocaleString); - t.equal(BigInt.prototype.toString, BigInt.prototype.toLocaleString); - t.equal(Date.prototype.toDateString, Date.prototype.toLocaleDateString); - t.equal(Date.prototype.toString, Date.prototype.toLocaleString); - t.equal(Date.prototype.toTimeString, Date.prototype.toLocaleTimeString); - t.equal(String.prototype.toLowerCase, String.prototype.toLocaleLowerCase); - t.equal(String.prototype.toUpperCase, String.prototype.toLocaleUpperCase); - t.equal(Array.prototype.toString, Array.prototype.toLocaleString); - - const TypedArray = Reflect.getPrototypeOf(Uint8Array); - t.equal(TypedArray.prototype.toString, TypedArray.prototype.toLocaleString); - - t.equal(typeof String.prototype.localeCompare, 'function'); - t.not(`${String.prototype.localeCompare}`.includes('[native code]')); - - t.end(); -}); diff --git a/packages/ses/test/tame-locale-methods-unsafe.test.js b/packages/ses/test/tame-locale-methods-unsafe.test.js new file mode 100644 index 0000000000..13588813d5 --- /dev/null +++ b/packages/ses/test/tame-locale-methods-unsafe.test.js @@ -0,0 +1,28 @@ +/* global lockdown BigInt */ +import test from 'tape'; +import '../src/main.js'; + +lockdown({ localeTaming: 'unsafe' }); + +test('tame locale methods', t => { + t.notEqual(Object.prototype.toString, Object.prototype.toLocaleString); + t.notEqual(Number.prototype.toString, Number.prototype.toLocaleString); + t.notEqual(BigInt.prototype.toString, BigInt.prototype.toLocaleString); + t.notEqual(Date.prototype.toDateString, Date.prototype.toLocaleDateString); + t.notEqual(Date.prototype.toString, Date.prototype.toLocaleString); + t.notEqual(Date.prototype.toTimeString, Date.prototype.toLocaleTimeString); + t.notEqual(String.prototype.toLowerCase, String.prototype.toLocaleLowerCase); + t.notEqual(String.prototype.toUpperCase, String.prototype.toLocaleUpperCase); + t.notEqual(Array.prototype.toString, Array.prototype.toLocaleString); + + const TypedArray = Reflect.getPrototypeOf(Uint8Array); + t.notEqual( + TypedArray.prototype.toString, + TypedArray.prototype.toLocaleString, + ); + + t.equal(typeof String.prototype.localeCompare, 'function'); + t.ok(`${String.prototype.localeCompare}`.includes('[native code]')); + + t.end(); +}); diff --git a/packages/ses/test/tame-locale-methods.test.js b/packages/ses/test/tame-locale-methods.test.js index 13588813d5..86aaab8557 100644 --- a/packages/ses/test/tame-locale-methods.test.js +++ b/packages/ses/test/tame-locale-methods.test.js @@ -2,27 +2,24 @@ import test from 'tape'; import '../src/main.js'; -lockdown({ localeTaming: 'unsafe' }); +lockdown(); test('tame locale methods', t => { - t.notEqual(Object.prototype.toString, Object.prototype.toLocaleString); - t.notEqual(Number.prototype.toString, Number.prototype.toLocaleString); - t.notEqual(BigInt.prototype.toString, BigInt.prototype.toLocaleString); - t.notEqual(Date.prototype.toDateString, Date.prototype.toLocaleDateString); - t.notEqual(Date.prototype.toString, Date.prototype.toLocaleString); - t.notEqual(Date.prototype.toTimeString, Date.prototype.toLocaleTimeString); - t.notEqual(String.prototype.toLowerCase, String.prototype.toLocaleLowerCase); - t.notEqual(String.prototype.toUpperCase, String.prototype.toLocaleUpperCase); - t.notEqual(Array.prototype.toString, Array.prototype.toLocaleString); + t.equal(Object.prototype.toString, Object.prototype.toLocaleString); + t.equal(Number.prototype.toString, Number.prototype.toLocaleString); + t.equal(BigInt.prototype.toString, BigInt.prototype.toLocaleString); + t.equal(Date.prototype.toDateString, Date.prototype.toLocaleDateString); + t.equal(Date.prototype.toString, Date.prototype.toLocaleString); + t.equal(Date.prototype.toTimeString, Date.prototype.toLocaleTimeString); + t.equal(String.prototype.toLowerCase, String.prototype.toLocaleLowerCase); + t.equal(String.prototype.toUpperCase, String.prototype.toLocaleUpperCase); + t.equal(Array.prototype.toString, Array.prototype.toLocaleString); const TypedArray = Reflect.getPrototypeOf(Uint8Array); - t.notEqual( - TypedArray.prototype.toString, - TypedArray.prototype.toLocaleString, - ); + t.equal(TypedArray.prototype.toString, TypedArray.prototype.toLocaleString); t.equal(typeof String.prototype.localeCompare, 'function'); - t.ok(`${String.prototype.localeCompare}`.includes('[native code]')); + t.not(`${String.prototype.localeCompare}`.includes('[native code]')); t.end(); }); diff --git a/packages/ses/test/tame-math-allow.test.js b/packages/ses/test/tame-math-allow.test.js deleted file mode 100644 index 92bac7de32..0000000000 --- a/packages/ses/test/tame-math-allow.test.js +++ /dev/null @@ -1,21 +0,0 @@ -/* global Compartment, lockdown */ -import test from 'tape'; -import '../src/main.js'; - -lockdown({ mathTaming: 'unsafe' }); - -test('lockdown() Math allowed - Math from Compartment is not tamed', t => { - const c = new Compartment(); - const random = c.evaluate('Math.random()'); - t.equal(typeof random, 'number'); - t.notOk(Number.isNaN(random)); - t.end(); -}); - -test('lockdown() Math allowed - Math from nested Compartment is not tamed', t => { - const c = new Compartment().evaluate('new Compartment()'); - const random = c.evaluate('Math.random()'); - t.equal(typeof random, 'number'); - t.notOk(Number.isNaN(random)); - t.end(); -}); diff --git a/packages/ses/test/tame-math-unit.test.js b/packages/ses/test/tame-math-unit.test.js new file mode 100644 index 0000000000..f1c647063e --- /dev/null +++ b/packages/ses/test/tame-math-unit.test.js @@ -0,0 +1,23 @@ +import tap from 'tap'; +import tameMathObject from '../src/tame-math-object.js'; + +const { test } = tap; + +const { + '%InitialMath%': initialMath, + '%SharedMath%': sharedMath, +} = tameMathObject(); + +test('tameMathObject - initial properties', t => { + t.equal(initialMath.random.name, 'random'); + + t.equal(typeof initialMath.random(), 'number'); + + t.end(); +}); + +test('tameMathObject - shared properties', t => { + t.equal(typeof sharedMath.random, 'undefined'); + + t.end(); +}); diff --git a/packages/ses/test/tame-math.test.js b/packages/ses/test/tame-math.test.js index b2cda64ff9..ef957e4434 100644 --- a/packages/ses/test/tame-math.test.js +++ b/packages/ses/test/tame-math.test.js @@ -4,14 +4,22 @@ import '../src/main.js'; lockdown(); -test('lockdown() default - Math from Compartment is tamed', t => { +test('lockdown start Math is powerful', t => { + t.equal(typeof Math.random, 'function'); + const random = Math.random(); + t.equal(typeof random, 'number'); + t.notOk(Number.isNaN(random)); + t.end(); +}); + +test('lockdown Math from Compartment is powerless', t => { const c = new Compartment(); - t.throws(() => c.evaluate('Math.random()')); + t.equal(c.evaluate('typeof Math.random'), 'undefined'); t.end(); }); -test('lockdown() default - Math from nested Compartment is tamed', t => { +test('lockdown Math from nested Compartment is powerless', t => { const c = new Compartment().evaluate('new Compartment()'); - t.throws(() => c.evaluate('Math.random()')); + t.equal(c.evaluate('typeof Math.random'), 'undefined'); t.end(); }); diff --git a/packages/ses/test/tame-global-reg-exp-object.test.js b/packages/ses/test/tame-regexp-unit.test.js similarity index 50% rename from packages/ses/test/tame-global-reg-exp-object.test.js rename to packages/ses/test/tame-regexp-unit.test.js index 42bd38f2ed..994d596eec 100644 --- a/packages/ses/test/tame-global-reg-exp-object.test.js +++ b/packages/ses/test/tame-regexp-unit.test.js @@ -1,22 +1,18 @@ import tap from 'tap'; -import tameGlobalRegExpObject from '../src/tame-global-reg-exp-object.js'; +import tameRegExpConstructor from '../src/tame-regexp-constructor.js'; const { test } = tap; const unsafeRegExp = RegExp; const { - start: { - RegExp: { value: tamedRegExp }, - }, - shared: { - RegExp: { value: sharedRegExp }, - }, -} = tameGlobalRegExpObject('safe'); + '%InitialRegExp%': InitialRegExp, + '%SharedRegExp%': SharedRegExp, +} = tameRegExpConstructor('safe'); -test('tameGlobalRegExpObject - unsafeRegExp denied', t => { - t.ok(unsafeRegExp !== tamedRegExp, 'constructor not replaced'); +test('tameRegExpConstructor - unsafeRegExp denied', t => { + t.ok(unsafeRegExp !== InitialRegExp, 'constructor not replaced'); const regexp = /./; - t.ok(regexp.constructor === sharedRegExp, 'tamed constructor not reached'); + t.ok(regexp.constructor === SharedRegExp, 'tamed constructor not reached'); // Don't leak the unsafe constructor // https://github.com/Agoric/SES-shim/issues/237 t.ok(regexp.constructor !== unsafeRegExp, 'unsafe constructor reachable!'); @@ -24,12 +20,11 @@ test('tameGlobalRegExpObject - unsafeRegExp denied', t => { t.end(); }); -test('tameGlobalRegExpObject - undeniable prototype', t => { +test('tameRegExpConstructor - undeniable prototype', t => { // Don't try to deny the undeniable // https://github.com/Agoric/SES-shim/issues/237 - // eslint-disable-next-line new-cap - const regexp1 = new tamedRegExp('.'); - const regexp2 = tamedRegExp('.'); + const regexp1 = new InitialRegExp('.'); + const regexp2 = InitialRegExp('.'); const regexp3 = /./; t.ok( // eslint-disable-next-line no-proto @@ -43,26 +38,26 @@ test('tameGlobalRegExpObject - undeniable prototype', t => { ); t.ok( - regexp1 instanceof tamedRegExp, + regexp1 instanceof InitialRegExp, 'new instance not instanceof tamed constructor', ); t.ok( - regexp2 instanceof tamedRegExp, + regexp2 instanceof InitialRegExp, 'non-new instance not instanceof tamed constructor', ); t.ok( - regexp3 instanceof tamedRegExp, + regexp3 instanceof InitialRegExp, 'literal instance not instanceof tamed constructor', ); t.end(); }); -test('tameGlobalRegExpObject - constructor', t => { - t.equal(tamedRegExp.name, 'RegExp'); - t.equal(tamedRegExp.prototype.constructor, sharedRegExp); +test('tameRegExpConstructor - constructor', t => { + t.equal(InitialRegExp.name, 'RegExp'); + t.equal(InitialRegExp.prototype.constructor, SharedRegExp); t.equal( - Object.getOwnPropertyDescriptor(tamedRegExp.prototype, 'compile'), + Object.getOwnPropertyDescriptor(InitialRegExp.prototype, 'compile'), undefined, ); @@ -72,24 +67,23 @@ test('tameGlobalRegExpObject - constructor', t => { 'prototype', Symbol.species, ]); - const properties = Reflect.ownKeys(tamedRegExp); + const properties = Reflect.ownKeys(InitialRegExp); for (const prop of properties) { - t.assert( + t.ok( allowedProperties.has(prop), `RegExp may not have static property ${String(prop)}`, ); } - // eslint-disable-next-line new-cap - const regexp = new tamedRegExp(); - t.ok(regexp instanceof tamedRegExp); + const regexp = new InitialRegExp(); + t.ok(regexp instanceof InitialRegExp); // eslint-disable-next-line no-proto - t.equal(regexp.__proto__.constructor, sharedRegExp); + t.equal(regexp.__proto__.constructor, SharedRegExp); - // bare tamedRegExp() (without 'new') was failing + // bare InitialRegExp() (without 'new') was failing // https://github.com/Agoric/SES-shim/issues/230 - t.equal(tamedRegExp('foo').test('bar'), false); - t.equal(tamedRegExp('foo').test('foobar'), true); + t.equal(InitialRegExp('foo').test('bar'), false); + t.equal(InitialRegExp('foo').test('foobar'), true); t.end(); }); diff --git a/packages/ses/test/tame-rexexp-allow.test.js b/packages/ses/test/tame-rexexp-allow.test.js deleted file mode 100644 index 5b72ee9810..0000000000 --- a/packages/ses/test/tame-rexexp-allow.test.js +++ /dev/null @@ -1,25 +0,0 @@ -/* global Compartment, lockdown */ -import test from 'tape'; -import '../src/main.js'; - -const unsafeRegExp = RegExp; - -lockdown({ regExpTaming: 'unsafe' }); - -test('lockdown() default - RegExp from Compartment is tamed', t => { - const c = new Compartment(); - const actualRegExp = c.evaluate('RegExp'); - - t.equal(actualRegExp, unsafeRegExp, 'RegExp should be native'); - - t.end(); -}); - -test('lockdown() default - RegExp from nested Compartment is tamed', t => { - const c = new Compartment().evaluate('new Compartment()'); - const actualRegExp = c.evaluate('RegExp'); - - t.equal(actualRegExp, unsafeRegExp, 'RegExp should be native'); - - t.end(); -}); diff --git a/packages/ses/test/tame-rexexp.test.js b/packages/ses/test/tame-rexexp.test.js index 2af5bdaabf..b4072f4dac 100644 --- a/packages/ses/test/tame-rexexp.test.js +++ b/packages/ses/test/tame-rexexp.test.js @@ -11,12 +11,12 @@ const allowedProperties = new Set([ lockdown(); -test('lockdown() RegExp allowed - RegExp from Compartment is not tamed', t => { +test('lockdown RegExp from Compartment is powerless', t => { const c = new Compartment(); const properties = c.evaluate('Reflect.ownKeys(RegExp)'); for (const prop of properties) { - t.assert( + t.ok( allowedProperties.has(prop), `RegExp may not have static property ${String(prop)}`, ); @@ -24,12 +24,12 @@ test('lockdown() RegExp allowed - RegExp from Compartment is not tamed', t => { t.end(); }); -test('lockdown() RegExp allowed - RegExp from nested Compartment not is tamed', t => { +test('lockdown RegExp from nested Compartment powerless', t => { const c = new Compartment().evaluate('new Compartment()'); const properties = c.evaluate('Reflect.ownKeys(RegExp)'); for (const prop of properties) { - t.assert( + t.ok( allowedProperties.has(prop), `RegExp may not have static property ${String(prop)}`, ); diff --git a/packages/ses/test/tame-v8-error-unit.test.js b/packages/ses/test/tame-v8-error-unit.test.js new file mode 100644 index 0000000000..0f40f13593 --- /dev/null +++ b/packages/ses/test/tame-v8-error-unit.test.js @@ -0,0 +1,22 @@ +import tap from 'tap'; +import tameErrorConstructor from '../src/tame-error-constructor.js'; + +const { test } = tap; + +const { '%InitialError%': InitialError } = tameErrorConstructor(); + +test('tameErrorConstructor', t => { + try { + t.equal(typeof InitialError.stackTraceLimit, 'number'); + InitialError.stackTraceLimit = 11; + t.equal(InitialError.stackTraceLimit, 11); + const error = new InitialError(); + t.equal(typeof error.stack, 'string'); + InitialError.captureStackTrace(error); + t.equal(typeof error.stack, 'string'); + } catch (e) { + t.isNot(e, e, 'unexpected exception'); + } finally { + t.end(); + } +}); diff --git a/packages/ses/test/error-manipulation.test.js b/packages/ses/test/tame-v8-error-unsafe.test.js similarity index 98% rename from packages/ses/test/error-manipulation.test.js rename to packages/ses/test/tame-v8-error-unsafe.test.js index 8115151475..c806dcfdc6 100644 --- a/packages/ses/test/error-manipulation.test.js +++ b/packages/ses/test/tame-v8-error-unsafe.test.js @@ -12,7 +12,7 @@ lockdown({ errorTaming: 'unsafe' }); // determine the call site of a deprecated function function simulateDepd() { - function prepareObjectStackTrace(obj, stack) { + function prepareObjectStackTrace(_, stack) { return stack; } diff --git a/packages/ses/test/tame-v8-error.test.js b/packages/ses/test/tame-v8-error.test.js new file mode 100644 index 0000000000..f94feb16b2 --- /dev/null +++ b/packages/ses/test/tame-v8-error.test.js @@ -0,0 +1,28 @@ +/* global Compartment, lockdown */ +import test from 'tape'; +import '../src/main.js'; + +lockdown(); + +test('lockdown Error is safe', t => { + t.equal(typeof Error.captureStackTrace, 'function'); + t.equal(typeof Error.stackTraceLimit, 'number'); + t.equal(typeof new Error().stack, 'string'); + t.end(); +}); + +test('lockdown Error in Compartment is safe', t => { + const c = new Compartment(); + t.equal(c.evaluate('typeof Error.captureStackTrace'), 'undefined'); + t.equal(c.evaluate('typeof Error.stackTraceLimit'), 'undefined'); + t.equal(c.evaluate('typeof new Error().stack'), 'string'); + t.end(); +}); + +test('lockdown Error in nested Compartment is safe', t => { + const c = new Compartment().evaluate('new Compartment()'); + t.equal(c.evaluate('typeof Error.captureStackTrace'), 'undefined'); + t.equal(c.evaluate('typeof Error.stackTraceLimit'), 'undefined'); + t.equal(c.evaluate('typeof new Error().stack'), 'string'); + t.end(); +}); diff --git a/packages/ses/test/whitelist-intrinsics.test.js b/packages/ses/test/whitelist-intrinsics.test.js index 49ece837d9..e2d463e060 100644 --- a/packages/ses/test/whitelist-intrinsics.test.js +++ b/packages/ses/test/whitelist-intrinsics.test.js @@ -1,9 +1,6 @@ import tap from 'tap'; -import tameGlobalErrorObject from '../src/tame-global-error-object.js'; -import { getIntrinsics } from '../src/intrinsics.js'; -import whitelistIntrinsics from '../src/whitelist-intrinsics.js'; - -const { defineProperties } = Object; +import '../src/main.js'; +import { repairIntrinsics } from '../src/lockdown-shim.js'; const { test } = tap; @@ -16,10 +13,6 @@ test('whitelistPrototypes - on', t => { // This test will modify intrinsics and should be executed // in a brand new realm. - // This changes the non-standard v8-only Error.prototype.stackTraceLimit - // to an accessor which matches our whitelist. Otherwise this test fails. - defineProperties(globalThis, tameGlobalErrorObject().start); - globalThis.foo = 1; Object.foo = 1; Object.freeze.foo = 1; @@ -27,13 +20,13 @@ test('whitelistPrototypes - on', t => { Object.prototype.foo = 1; Object.prototype.hasOwnProperty.foo = 1; - console.time('Benchmark getIntrinsics()'); - const intrinsics = getIntrinsics(); - console.timeEnd('Benchmark getIntrinsics()'); + console.time('Benchmark repairIntrinsics()'); + const hardenIntrinsics = repairIntrinsics(); + console.timeEnd('Benchmark repairIntrinsics()'); - console.time('Benchmark whitelistIntrinsics()'); - whitelistIntrinsics(intrinsics); - console.timeEnd('Benchmark whitelistIntrinsics()'); + console.time('Benchmark hardenIntrinsics()'); + hardenIntrinsics(); + console.timeEnd('Benchmark hardenIntrinsics()'); t.equal(globalThis.foo, 1); t.equal(Object.foo, undefined); diff --git a/packages/ses/test262/tame-global-date-object.js b/packages/ses/test262/tame-global-date-object.js index dfa6cc70dd..7412510740 100644 --- a/packages/ses/test262/tame-global-date-object.js +++ b/packages/ses/test262/tame-global-date-object.js @@ -1,8 +1,6 @@ // eslint-disable-next-line import/no-extraneous-dependencies import test262Runner from '@agoric/test262-runner'; -import tameGlobalDateObject from '../src/tame-global-date-object.js'; - -const { defineProperties } = Object; +import tameDateConstructor from '../src/tame-date-constructor.js'; test262Runner({ testDirs: ['/test/built-ins/Date'], @@ -18,7 +16,7 @@ test262Runner({ sourceTextCorrections: [], captureGlobalObjectNames: ['Date'], async test(testInfo, harness) { - defineProperties(globalThis, tameGlobalDateObject('unsafe').start); + globalThis.Date = tameDateConstructor()['%InitialDate%']; // eslint-disable-next-line no-eval (0, eval)(`${harness}\n${testInfo.contents}`); }, diff --git a/packages/ses/test262/tame-global-error-object.js b/packages/ses/test262/tame-global-error-object.js index e052d0d18f..79c8d8e254 100644 --- a/packages/ses/test262/tame-global-error-object.js +++ b/packages/ses/test262/tame-global-error-object.js @@ -1,12 +1,15 @@ // eslint-disable-next-line import/no-extraneous-dependencies import test262Runner from '@agoric/test262-runner'; -import tameGlobalErrorObject from '../src/tame-global-error-object.js'; - -const { defineProperties } = Object; +import tameErrorConstructor from '../src/tame-error-constructor.js'; test262Runner({ testDirs: ['/test/built-ins/Error'], - excludePaths: [], + excludePaths: [ + // Excluded because Error.prototype.constructor is SharedError + 'test/built-ins/Error/S15.11.1_A1_T1.js', + 'test/built-ins/Error/prototype/constructor/S15.11.4.1_A1_T1.js', + 'test/built-ins/Error/prototype/constructor/S15.11.4.1_A1_T2.js', + ], excludeDescriptions: [], excludeFeatures: [ 'cross-realm', // TODO: Evaluator does not create realms. @@ -18,7 +21,7 @@ test262Runner({ sourceTextCorrections: [], captureGlobalObjectNames: ['Error'], async test(testInfo, harness) { - defineProperties(globalThis, tameGlobalErrorObject().start); + globalThis.Error = tameErrorConstructor()['%InitialError%']; // eslint-disable-next-line no-eval (0, eval)(`${harness}\n${testInfo.contents}`); }, diff --git a/packages/ses/test262/tame-global-math-object.js b/packages/ses/test262/tame-global-math-object.js index 5fc5c8d423..ff770c9b45 100644 --- a/packages/ses/test262/tame-global-math-object.js +++ b/packages/ses/test262/tame-global-math-object.js @@ -1,8 +1,6 @@ // eslint-disable-next-line import/no-extraneous-dependencies import test262Runner from '@agoric/test262-runner'; -import tameGlobalMathObject from '../src/tame-global-math-object.js'; - -const { defineProperties } = Object; +import tameMathObject from '../src/tame-math-object.js'; test262Runner({ testDirs: ['/test/built-ins/Math'], @@ -18,7 +16,7 @@ test262Runner({ sourceTextCorrections: [], captureGlobalObjectNames: ['Math'], async test(testInfo, harness) { - defineProperties(globalThis, tameGlobalMathObject().start); + globalThis.Math = tameMathObject()['%InitialMath%']; // eslint-disable-next-line no-eval (0, eval)(`${harness}\n${testInfo.contents}`); }, diff --git a/packages/ses/test262/tame-global-regexp-object.js b/packages/ses/test262/tame-global-regexp-object.js index 3ce8269579..ce62a65de3 100644 --- a/packages/ses/test262/tame-global-regexp-object.js +++ b/packages/ses/test262/tame-global-regexp-object.js @@ -1,8 +1,6 @@ // eslint-disable-next-line import/no-extraneous-dependencies import test262Runner from '@agoric/test262-runner'; -import tameGlobalRegExpObject from '../src/tame-global-reg-exp-object.js'; - -const { defineProperties } = Object; +import tameRegExpConstructor from '../src/tame-regexp-constructor.js'; test262Runner({ testDirs: ['/test/built-ins/RegExp'], @@ -21,6 +19,14 @@ test262Runner({ 'test/built-ins/RegExp/S15.10.3.1_A1_T4.js', 'test/built-ins/RegExp/S15.10.3.1_A1_T5.js', 'test/built-ins/RegExp/S15.10.3.1_A2_T2.js', + + // Excluded because RegExp.prototype.constructor is SharedRegExp + 'test/built-ins/RegExp/S15.10.3.1_A3_T1.js', + 'test/built-ins/RegExp/S15.10.3.1_A3_T2.js', + 'test/built-ins/RegExp/S15.10.7_A3_T1.js', + 'test/built-ins/RegExp/S15.10.7_A3_T2.js', + 'test/built-ins/RegExp/prototype/S15.10.6.1_A1_T1.js', + 'test/built-ins/RegExp/prototype/S15.10.6.1_A1_T2.js', ], excludeDescriptions: [], excludeFeatures: [ @@ -33,7 +39,7 @@ test262Runner({ sourceTextCorrections: [], captureGlobalObjectNames: ['RegExp'], async test(testInfo, harness) { - defineProperties(globalThis, tameGlobalRegExpObject().start); + globalThis.RegExp = tameRegExpConstructor()['%InitialRegExp%']; // eslint-disable-next-line no-eval (0, eval)(`${harness}\n${testInfo.contents}`); },