ses
Version:
Hardened JavaScript for Fearless Cooperation
500 lines (443 loc) • 18.6 kB
JavaScript
// Copyright (C) 2018 Agoric
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// @ts-check
import { getEnvironmentOption as getenv } from '@endo/env-options';
import {
FERAL_FUNCTION,
FERAL_EVAL,
TypeError,
arrayFilter,
globalThis,
is,
ownKeys,
stringSplit,
noEvalEvaluate,
getOwnPropertyNames,
getPrototypeOf,
} from './commons.js';
import { makeHardener } from './make-hardener.js';
import { makeIntrinsicsCollector } from './intrinsics.js';
import removeUnpermittedIntrinsics from './permits-intrinsics.js';
import tameFunctionConstructors from './tame-function-constructors.js';
import tameDateConstructor from './tame-date-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 {
setGlobalObjectConstantProperties,
setGlobalObjectMutableProperties,
setGlobalObjectEvaluators,
} from './global-object.js';
import { makeSafeEvaluator } from './make-safe-evaluator.js';
import { initialGlobalPropertyNames } from './permits.js';
import { tameFunctionToString } from './tame-function-tostring.js';
import { tameDomains } from './tame-domains.js';
import { tameModuleSource } from './tame-module-source.js';
import { tameConsole } from './error/tame-console.js';
import tameErrorConstructor from './error/tame-error-constructor.js';
import { assert, makeAssert } from './error/assert.js';
import { getAnonymousIntrinsics } from './get-anonymous-intrinsics.js';
import { makeCompartmentConstructor } from './compartment.js';
import { tameHarden } from './tame-harden.js';
import { tameSymbolConstructor } from './tame-symbol-constructor.js';
import { tameFauxDataProperties } from './tame-faux-data-properties.js';
import { tameRegeneratorRuntime } from './tame-regenerator-runtime.js';
import { shimArrayBufferTransfer } from './shim-arraybuffer-transfer.js';
import { reportInGroup, chooseReporter } from './reporting.js';
/** @import {LockdownOptions} from '../types.js' */
const { Fail, details: X, quote: q } = assert;
/** @type {Error=} */
let priorRepairIntrinsics;
/** @type {Error=} */
let priorHardenIntrinsics;
// Build a harden() with an empty fringe.
// Gate it on lockdown.
/**
* @template T
* @param {T} ref
* @returns {T}
*/
const safeHarden = makeHardener();
/**
* @callback Transform
* @param {string} source
* @returns {string}
*/
/**
* @callback CompartmentConstructor
* @param {object} endowments
* @param {object} moduleMap
* @param {object} [options]
* @param {Array<Transform>} [options.transforms]
* @param {Array<Transform>} [options.__shimTransforms__]
*/
// TODO https://github.com/endojs/endo/issues/814
// Lockdown currently allows multiple calls provided that the specified options
// of every call agree. With experience, we have observed that lockdown should
// only ever need to be called once and that simplifying lockdown will improve
// the quality of audits.
const assertDirectEvalAvailable = () => {
let allowed = false;
try {
allowed = FERAL_FUNCTION(
'eval',
'SES_changed',
`\
eval("SES_changed = true");
return SES_changed;
`,
)(FERAL_EVAL, false);
// If we get here and SES_changed stayed false, that means the eval was sloppy
// and indirect, which generally creates a new global.
// We are going to throw an exception for failing to initialize SES, but
// good neighbors clean up.
if (!allowed) {
delete globalThis.SES_changed;
}
} catch (_error) {
// We reach here if eval is outright forbidden by a Content Security Policy.
// We allow this for SES usage that delegates the responsibility to isolate
// guest code to production code generation.
allowed = true;
}
if (!allowed) {
// See https://github.com/endojs/endo/blob/master/packages/ses/error-codes/SES_DIRECT_EVAL.md
throw TypeError(
`SES cannot initialize unless 'eval' is the original intrinsic 'eval', suitable for direct-eval (dynamically scoped eval) (SES_DIRECT_EVAL)`,
);
}
};
/**
* @param {LockdownOptions} [options]
*/
export const 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.
// Reconstructing `option` here also ensures that it is a well
// behaved record, with only own data properties.
//
// The `overrideTaming` is not a safety issue. Rather it is a tradeoff
// between code compatibility, which is better with the `'moderate'`
// setting, and tool compatibility, which is better with the `'min'`
// setting. See
// https://github.com/Agoric/SES-shim/blob/master/packages/ses/README.md#enabling-override-by-assignment)
// for an explanation of when to use which.
//
// The `stackFiltering` is not a safety issue. Rather it is a tradeoff
// between relevance and completeness of the stack frames shown on the
// console. Setting`stackFiltering` to `'verbose'` applies no filters, providing
// the raw stack frames that can be quite versbose. Setting
// `stackFrameFiltering` to`'concise'` limits the display to the stack frame
// information most likely to be relevant, eliminating distracting frames
// such as those from the infrastructure. However, the bug you're trying to
// track down might be in the infrastrure, in which case the `'verbose'` setting
// is useful. See
// [`stackFiltering` options](https://github.com/Agoric/SES-shim/blob/master/packages/ses/docs/lockdown.md#stackfiltering-options)
// for an explanation.
const {
errorTaming = getenv('LOCKDOWN_ERROR_TAMING', 'safe'),
errorTrapping = /** @type {"platform" | "none" | "report" | "abort" | "exit"} */ (
getenv('LOCKDOWN_ERROR_TRAPPING', 'platform')
),
reporting = /** @type {"platform" | "console" | "none"} */ (
getenv('LOCKDOWN_REPORTING', 'platform')
),
unhandledRejectionTrapping = /** @type {"none" | "report"} */ (
getenv('LOCKDOWN_UNHANDLED_REJECTION_TRAPPING', 'report')
),
regExpTaming = getenv('LOCKDOWN_REGEXP_TAMING', 'safe'),
localeTaming = getenv('LOCKDOWN_LOCALE_TAMING', 'safe'),
consoleTaming = /** @type {'unsafe' | 'safe'} */ (
getenv('LOCKDOWN_CONSOLE_TAMING', 'safe')
),
overrideTaming = /** @type {'moderate' | 'min' | 'severe'} */ (
getenv('LOCKDOWN_OVERRIDE_TAMING', 'moderate')
),
stackFiltering = getenv('LOCKDOWN_STACK_FILTERING', 'concise'),
domainTaming = getenv('LOCKDOWN_DOMAIN_TAMING', 'safe'),
evalTaming = getenv('LOCKDOWN_EVAL_TAMING', 'safeEval'),
overrideDebug = arrayFilter(
stringSplit(getenv('LOCKDOWN_OVERRIDE_DEBUG', ''), ','),
/** @param {string} debugName */
debugName => debugName !== '',
),
legacyRegeneratorRuntimeTaming = getenv(
'LOCKDOWN_LEGACY_REGENERATOR_RUNTIME_TAMING',
'safe',
),
__hardenTaming__ = getenv('LOCKDOWN_HARDEN_TAMING', 'safe'),
dateTaming, // deprecated
mathTaming, // deprecated
...extraOptions
} = options;
legacyRegeneratorRuntimeTaming === 'safe' ||
legacyRegeneratorRuntimeTaming === 'unsafe-ignore' ||
Fail`lockdown(): non supported option legacyRegeneratorRuntimeTaming: ${q(legacyRegeneratorRuntimeTaming)}`;
evalTaming === 'unsafeEval' ||
evalTaming === 'safeEval' ||
evalTaming === 'noEval' ||
Fail`lockdown(): non supported option evalTaming: ${q(evalTaming)}`;
// Assert that only supported options were passed.
// Use Reflect.ownKeys to reject symbol-named properties as well.
const extraOptionsNames = ownKeys(extraOptions);
extraOptionsNames.length === 0 ||
Fail`lockdown(): non supported option ${q(extraOptionsNames)}`;
const reporter = chooseReporter(reporting);
const { warn } = reporter;
if (dateTaming !== undefined) {
// eslint-disable-next-line no-console
warn(
`SES The 'dateTaming' option is deprecated and does nothing. In the future specifying it will be an error.`,
);
}
if (mathTaming !== undefined) {
// eslint-disable-next-line no-console
warn(
`SES The 'mathTaming' option is deprecated and does nothing. In the future specifying it will be an error.`,
);
}
priorRepairIntrinsics === undefined ||
// eslint-disable-next-line @endo/no-polymorphic-call
assert.fail(
X`Already locked down at ${priorRepairIntrinsics} (SES_ALREADY_LOCKED_DOWN)`,
TypeError,
);
// See https://github.com/endojs/endo/blob/master/packages/ses/error-codes/SES_ALREADY_LOCKED_DOWN.md
priorRepairIntrinsics = TypeError('Prior lockdown (SES_ALREADY_LOCKED_DOWN)');
// Tease V8 to generate the stack string and release the closures the stack
// trace retained:
priorRepairIntrinsics.stack;
assertDirectEvalAvailable();
/**
* Because of packagers and bundlers, etc, multiple invocations of lockdown
* might happen in separate instantiations of the source of this module.
* In that case, each one sees its own `firstOptions` variable, so the test
* above will not detect that lockdown has already happened. We
* unreliably test some telltale signs that lockdown has run, to avoid
* trying to lock down a locked down environment. Although the test is
* unreliable, this is consistent with the SES threat model. SES provides
* security only if it runs first in a given realm, or if everything that
* runs before it is SES-aware and cooperative. Neither SES nor anything
* can protect itself from corrupting code that runs first. For these
* purposes, code that turns a realm into something that passes these
* tests without actually locking down counts as corrupting code.
*
* The specifics of what this tests for may change over time, but it
* should be consistent with any setting of the lockdown options.
*/
const seemsToBeLockedDown = () => {
return (
globalThis.Function.prototype.constructor !== globalThis.Function &&
// @ts-ignore harden is absent on globalThis type def.
typeof globalThis.harden === 'function' &&
// @ts-ignore lockdown is absent on globalThis type def.
typeof globalThis.lockdown === 'function' &&
globalThis.Date.prototype.constructor !== globalThis.Date &&
typeof globalThis.Date.now === 'function' &&
// @ts-ignore does not recognize that Date constructor is a special
// Function.
// eslint-disable-next-line @endo/no-polymorphic-call
is(globalThis.Date.prototype.constructor.now(), NaN)
);
};
if (seemsToBeLockedDown()) {
// See https://github.com/endojs/endo/blob/master/packages/ses/error-codes/SES_MULTIPLE_INSTANCES.md
throw TypeError(
`Already locked down but not by this SES instance (SES_MULTIPLE_INSTANCES)`,
);
}
/**
* 1. TAME powers & gather intrinsics first.
*/
tameDomains(domainTaming);
// Replace Function.prototype.toString with one that recognizes
// shimmed functions as honorary native functions.
const markVirtualizedNativeFunction = tameFunctionToString();
const { addIntrinsics, completePrototypes, finalIntrinsics } =
makeIntrinsicsCollector(reporter);
// @ts-expect-error __hardenTaming__ could be any string
const tamedHarden = tameHarden(safeHarden, __hardenTaming__);
addIntrinsics({ harden: tamedHarden });
addIntrinsics(tameFunctionConstructors());
addIntrinsics(tameDateConstructor());
addIntrinsics(tameErrorConstructor(errorTaming, stackFiltering));
addIntrinsics(tameMathObject());
addIntrinsics(tameRegExpConstructor(regExpTaming));
addIntrinsics(tameSymbolConstructor());
addIntrinsics(shimArrayBufferTransfer());
addIntrinsics(tameModuleSource());
addIntrinsics(getAnonymousIntrinsics());
completePrototypes();
const intrinsics = finalIntrinsics();
const hostIntrinsics = { __proto__: null };
// The Node.js Buffer is a derived class of Uint8Array, and as such is often
// passed around where a Uint8Array is expected.
if (typeof globalThis.Buffer === 'function') {
hostIntrinsics.Buffer = globalThis.Buffer;
}
/**
* Wrap console unless suppressed.
* At the moment, the console is considered a host power in the start
* compartment, and not a primordial. Hence it is absent from the whilelist
* and bypasses the intrinsicsCollector.
*
* @type {((error: any) => string | undefined) | undefined}
*/
let optGetStackString;
if (errorTaming === 'safe') {
optGetStackString = intrinsics['%InitialGetStackString%'];
}
const consoleRecord = tameConsole(
consoleTaming,
errorTrapping,
unhandledRejectionTrapping,
optGetStackString,
);
globalThis.console = /** @type {Console} */ (consoleRecord.console);
// The untamed Node.js console cannot itself be hardened as it has mutable
// internal properties, but some of these properties expose internal versions
// of classes from node's "primordials" concept.
// eslint-disable-next-line no-underscore-dangle
if (typeof (/** @type {any} */ (consoleRecord.console)._times) === 'object') {
// SafeMap is a derived Map class used internally by Node
// There doesn't seem to be a cleaner way to reach it.
hostIntrinsics.SafeMap = getPrototypeOf(
// eslint-disable-next-line no-underscore-dangle
/** @type {any} */ (consoleRecord.console)._times,
);
}
// @ts-ignore assert is absent on globalThis type def.
if (
(errorTaming === 'unsafe' || errorTaming === 'unsafe-debug') &&
globalThis.assert === assert
) {
// If errorTaming is 'unsafe' or 'unsafe-debug' we replace the
// global assert with
// one whose `details` template literal tag does not redact
// unmarked substitution values. IOW, it blabs information that
// was supposed to be secret from callers, as an aid to debugging
// at a further cost in safety.
// @ts-ignore assert is absent on globalThis type def.
globalThis.assert = makeAssert(undefined, true);
}
// Replace *Locale* methods with their non-locale equivalents
tameLocaleMethods(intrinsics, localeTaming);
tameFauxDataProperties(intrinsics);
/**
* 2. Enforce PERMITS on shared intrinsics
*/
// Remove non-standard properties.
// All remaining functions encountered during whitelisting are
// branded as honorary native functions.
reportInGroup(
'SES Removing unpermitted intrinsics',
reporter,
groupReporter =>
removeUnpermittedIntrinsics(
intrinsics,
markVirtualizedNativeFunction,
groupReporter,
),
);
// Initialize the powerful initial global, i.e., the global of the
// start compartment, from the intrinsics.
setGlobalObjectConstantProperties(globalThis);
setGlobalObjectMutableProperties(globalThis, {
intrinsics,
newGlobalPropertyNames: initialGlobalPropertyNames,
makeCompartmentConstructor,
markVirtualizedNativeFunction,
});
if (evalTaming === 'noEval') {
setGlobalObjectEvaluators(
globalThis,
noEvalEvaluate,
markVirtualizedNativeFunction,
);
} else if (evalTaming === 'safeEval') {
const { safeEvaluate } = makeSafeEvaluator({ globalObject: globalThis });
setGlobalObjectEvaluators(
globalThis,
safeEvaluate,
markVirtualizedNativeFunction,
);
} else if (evalTaming === 'unsafeEval') {
// Leave eval function and Function constructor of the initial compartment in-tact.
// Other compartments will not have access to these evaluators unless a guest program
// escapes containment.
}
/**
* 3. HARDEN to share the intrinsics.
*
* We define hardenIntrinsics here so that options are in scope, but return
* it to the caller because we intend to eventually allow vetted shims to run
* between repairs and the hardening of intrinsics and so we can benchmark
* repair separately from hardening.
*/
const hardenIntrinsics = () => {
priorHardenIntrinsics === undefined ||
// eslint-disable-next-line @endo/no-polymorphic-call
assert.fail(
X`Already locked down at ${priorHardenIntrinsics} (SES_ALREADY_LOCKED_DOWN)`,
TypeError,
);
// See https://github.com/endojs/endo/blob/master/packages/ses/error-codes/SES_ALREADY_LOCKED_DOWN.md
priorHardenIntrinsics = TypeError(
'Prior lockdown (SES_ALREADY_LOCKED_DOWN)',
);
// Tease V8 to generate the stack string and release the closures the stack
// trace retained:
priorHardenIntrinsics.stack;
// Circumvent the override mistake.
// TODO consider moving this to the end of the repair phase, and
// therefore before vetted shims rather than afterwards. It is not
// clear yet which is better.
// @ts-ignore enablePropertyOverrides does its own input validation
reportInGroup('SES Enabling property overrides', reporter, groupReporter =>
enablePropertyOverrides(
intrinsics,
overrideTaming,
groupReporter,
overrideDebug,
),
);
if (legacyRegeneratorRuntimeTaming === 'unsafe-ignore') {
tameRegeneratorRuntime();
}
// Finally register and optionally freeze all the intrinsics. This
// must be the operation that modifies the intrinsics.
const toHarden = {
intrinsics,
hostIntrinsics,
globals: {
// Harden evaluators
Function: globalThis.Function,
eval: globalThis.eval,
// @ts-ignore Compartment does exist on globalThis
Compartment: globalThis.Compartment,
// Harden Symbol
Symbol: globalThis.Symbol,
},
};
// Harden Symbol and properties for initialGlobalPropertyNames in the host realm
for (const prop of getOwnPropertyNames(initialGlobalPropertyNames)) {
toHarden.globals[prop] = globalThis[prop];
}
tamedHarden(toHarden);
return tamedHarden;
};
return hardenIntrinsics;
};