feat(ses): Read lockdown options from environment
*BREAKING CHANGE*: This may break applications that depend on lockdown
being idempotent, being able to call it multiple times.
MODIFY
packages/ses-integration-test/puppeteer-test/utility/test-bundler.js
packages/ses-integration-test/puppeteer-test/utility/test-bundler.js
@@ -6,6 +6,13 @@ import test from 'tape-promise/tape';
import path from 'path';
const runBrowserTests = async (t, indexFile) => {
+ // When this test fails with SES_NO_SLOPPY, it may indicate that the bundler,
+ // often just Parcel, inferred from access to a Node.js global object like
+ // process, which it then shimmed. This alters the shape of the bundle such
+ // that SES does not execute in strict mode. The remediation is usually to
+ // change the form "process" or "global.process" to "globalThis.process", or
+ // similar.
+
const browser = await puppeteer.launch({
// debug:
// { headless: false }

MODIFY
packages/ses/NEWS.md
packages/ses/NEWS.md
User-visible changes in SES:
+# Next release
+
+- Lockdown will now read options from the environment as defined by the Node.js
+ `process.env` parameter space.
+- *BREAKING CHANGE*: It may no longer be safe to call `lockdown` more than once
+ per realm (VM context or iframe).
+
# 0.14.4 (2021-10-14)
- Fixes a defect in the per-compartment `Function` and `eval` functions, such

MODIFY
packages/ses/lockdown-options.md
packages/ses/lockdown-options.md
@@ -33,6 +33,10 @@ Each option is explained in its own section below.
| `domainTaming` | `'unsafe'` | `'safe'` | bans the Node.js `domain` module, to be `'safe'` by default in the next breaking-changes release |
| `__allowUnsafeMonkeyPatching__` | `'safe'` | `'unsafe'` | run unsafe code unsafely |
+In the absence of any of these options in lockdown arguments, lockdown will
+attempt to read these from the Node.js `process.env` environment.
+The corresponding environment variables are the same but in `UPPER_SNAKE_CASE`.
+
The options `mathTaming` and `dateTaming` are deprecated.
`Math.random`, `Date.now`, and the `new Date()` are disabled within
compartments and can be injected as `globalThis` endowments if necessary, as in

ADD
packages/ses/src/environment-options.js
+// @ts-check
+
+// environment-options needs to be imported quite early, and so should
+// avoid importing from anything but commons.js and assert.js
+import { arrayPush, arraySlice, freeze } from './commons.js';
+import { assert } from './error/assert.js';
+
+const { details: X, quote: q } = assert;
+
+/**
+ * JavaScript module semantics resists attempts to parameterize a module's
+ * initialization behavior. A module initializes in order according to
+ * the path by which it is first imported, and then the initialized module
+ * is reused by all the other times it is imported. Compartments give us
+ * the opportunity to bind the same import name to different imported
+ * modules, depending on the package/compartment doing the import. Compartments
+ * also address the difficulty of parameterizing a module's initialization
+ * logic, but not in a pleasant manner.
+ *
+ * A pleasant parameterization would be for a static module to be function-like
+ * with explicit parameters, and for the parameterization to be like
+ * calling the static module with parameters in order to derive from it a
+ * module instance. Compartments instead lets us parameterize the meaning
+ * of a module instance derived from a static module according to the
+ * three namespaces provided by the JavaScript semantics, affecting the
+ * meaning of a module instance.
+ * * The global variable namespaces.
+ * * The global scope, aliased to properties of the global object.
+ * This is necessarily compartment-wide, and therefore in our
+ * recommened usage pattern, package-wide.
+ * * The global lexical scope. The SES-shim compartments support
+ * these both compartment-wide as well as per-module. But it is
+ * not yet clear what we will propose in the Compartment proposal.
+ * * The import namespace.
+ * * The host hooks.
+ *
+ * This `environment-options.js` module looks for a setting of an
+ * `optionName` parameter rooted in the global scope. If follows the Node
+ * precedent for finding Unix environment variable settings, looking for a
+ * global `process` object holding an `env` object,
+ * optionally holding a property named for the `optionName` whose value is the
+ * configuration setting of that option. For example, for the optionName
+ * `ALLOW_IMPLICIT_REMOTABLES` it would look in
+ * `globalThis.process.env.ALLOW_IMPLICIT_REMOTABLES`.
+ *
+ * If setting is either absent or `undefined`, that indicates that
+ * this configuration option should have its default behavior, whatever that is.
+ * Otherwise, reflecting Unix environment variables, the setting must be a
+ * string. This also helps ensure that this channel is used only to pass data,
+ * not authority beyond the ability to read this global state.
+ */
+
+/**
+ * makeEnvironmentCaptor provides a mechanism for getting environment
+ * variables, if they are needed, and a way to catalog the names of all
+ * the environment variables that were captured.
+ *
+ * @param {Object} aGlobal
+ */
+export const makeEnvironmentCaptor = aGlobal => {
+ const capturedEnvironmentOptionNames = [];
+
+ /**
+ * Gets an environment option by name and returns the option value or the
+ * given default.
+ *
+ * @param {string} optionName
+ * @param {string} defaultSetting
+ * @returns {string}
+ */
+ const getEnvironmentOption = (optionName, defaultSetting) => {
+ // eslint-disable-next-line @endo/no-polymorphic-call
+ assert.typeof(
+ optionName,
+ 'string',
+ X`Environment option name ${q(optionName)} must be a string.`,
+ );
+ // eslint-disable-next-line @endo/no-polymorphic-call
+ assert.typeof(
+ defaultSetting,
+ 'string',
+ X`Environment option default setting ${q(
+ defaultSetting,
+ )} must be a string.`,
+ );
+
+ /** @type {string} */
+ let setting = defaultSetting;
+ const globalProcess = aGlobal.process;
+ if (globalProcess && typeof globalProcess === 'object') {
+ const globalEnv = globalProcess.env;
+ if (globalEnv && typeof globalEnv === 'object') {
+ if (optionName in globalEnv) {
+ arrayPush(capturedEnvironmentOptionNames, optionName);
+ const optionValue = globalEnv[optionName];
+ // eslint-disable-next-line @endo/no-polymorphic-call
+ assert.typeof(
+ optionValue,
+ 'string',
+ X`Environment option named ${q(
+ optionName,
+ )}, if present, must have a corresponding string value, got ${q(
+ optionValue,
+ )}`,
+ );
+ setting = optionValue;
+ }
+ }
+ }
+ assert(
+ setting === undefined || typeof setting === 'string',
+ X`Environment option value ${q(setting)}, if present, must be a string.`,
+ );
+ return setting;
+ };
+ freeze(getEnvironmentOption);
+
+ const getCapturedEnvironmentOptionNames = () => {
+ return freeze(arraySlice(capturedEnvironmentOptionNames));
+ };
+ freeze(getCapturedEnvironmentOptionNames);
+
+ return { getEnvironmentOption, getCapturedEnvironmentOptionNames };
+};
+freeze(makeEnvironmentCaptor);

MODIFY
packages/ses/src/error/stringify-utils.js
packages/ses/src/error/stringify-utils.js
import {
Set,
String,
+ arrayJoin,
+ arraySlice,
freeze,
is,
isError,
setAdd,
setHas,
- stringStartsWith,
stringIncludes,
+ stringStartsWith,
stringifyJson,
toStringTagSymbol,
} from '../commons.js';
+/**
+ * Joins English terms with commas and an optional conjunction.
+ *
+ * @param {string[]} terms
+ * @param {"and" | "or"} conjunction
+ */
+export const enJoin = (terms, conjunction) => {
+ if (terms.length === 0) {
+ return '(none)';
+ } else if (terms.length === 1) {
+ return terms[0];
+ } else if (terms.length === 2) {
+ const [first, second] = terms;
+ return `${first} ${conjunction} ${second}`;
+ } else {
+ return `${arrayJoin(arraySlice(terms, 0, -1), ', ')}, ${conjunction} ${
+ terms[terms.length - 1]
+ }`;
+ }
+};
+
/**
* Prepend the correct indefinite article onto a noun, typically a typeof
* result, e.g., "an object" vs. "a number"

MODIFY
packages/ses/src/lockdown-shim.js
packages/ses/src/lockdown-shim.js
// @ts-check
-import { globalThis, is, keys, ownKeys } from './commons.js';
+import {
+ arrayFilter,
+ arrayMap,
+ globalThis,
+ is,
+ keys,
+ ownKeys,
+ stringSplit,
+} from './commons.js';
+import { enJoin } from './error/stringify-utils.js';
import { makeHardener } from './make-hardener.js';
import { makeIntrinsicsCollector } from './intrinsics.js';
import whitelistIntrinsics from './whitelist-intrinsics.js';
@@ -36,6 +45,7 @@ import { tameDomains } from './tame-domains.js';
import { tameConsole } from './error/tame-console.js';
import tameErrorConstructor from './error/tame-error-constructor.js';
import { assert, makeAssert } from './error/assert.js';
+import { makeEnvironmentCaptor } from './environment-options.js';
/** @typedef {import('../index.js').LockdownOptions} LockdownOptions */
@@ -133,23 +143,47 @@ export const repairIntrinsics = (
// [`stackFiltering` options](https://github.com/Agoric/SES-shim/blob/master/packages/ses/lockdown-options.md#stackfiltering-options)
// for an explanation.
options = /** @type {LockdownOptions} */ ({ ...firstOptions, ...options });
+
const {
+ getEnvironmentOption: getenv,
+ getCapturedEnvironmentOptionNames,
+ } = makeEnvironmentCaptor(globalThis);
+
+ const {
+ errorTaming = getenv('LOCKDOWN_ERROR_TAMING', 'safe'),
+ errorTrapping = getenv('LOCKDOWN_ERROR_TRAPPING', 'platform'),
+ regExpTaming = getenv('LOCKDOWN_REGEXP_TAMING', 'safe'),
+ localeTaming = getenv('LOCKDOWN_LOCALE_TAMING', 'safe'),
+ consoleTaming = getenv('LOCKDOWN_CONSOLE_TAMING', 'safe'),
+ overrideTaming = getenv('LOCKDOWN_OVERRIDE_TAMING', 'moderate'),
+ stackFiltering = getenv('LOCKDOWN_STACK_FILTERING', 'concise'),
+ // TODO domainTaming should change to safe-by-default in the next breaking relase.
+ domainTaming = getenv('LOCKDOWN_DOMAIN_TAMING', 'unsafe'),
+ overrideDebug = arrayFilter(
+ stringSplit(getenv('LOCKDOWN_OVERRIDE_DEBUG', ''), ','),
+ /** @param {string} debugName */
+ debugName => debugName !== '',
+ ),
+ __allowUnsafeMonkeyPatching__ = getenv(
+ '__LOCKDOWN_ALLOW_UNSAFE_MONKEY_PATCHING__',
+ 'safe',
+ ),
dateTaming = 'safe', // deprecated
- errorTaming = 'safe',
mathTaming = 'safe', // deprecated
- errorTrapping = 'platform',
- regExpTaming = 'safe',
- localeTaming = 'safe',
- consoleTaming = 'safe',
- overrideTaming = 'moderate',
- overrideDebug = [],
- stackFiltering = 'concise',
- domainTaming = 'unsafe', // TODO become 'safe' by default in next-breaking-release.
- __allowUnsafeMonkeyPatching__ = 'safe',
-
...extraOptions
} = options;
+ const capturedEnvironmentOptionNames = getCapturedEnvironmentOptionNames();
+ if (capturedEnvironmentOptionNames.length > 0) {
+ // eslint-disable-next-line @endo/no-polymorphic-call
+ console.warn(
+ `SES Lockdown using options from environment variables ${enJoin(
+ arrayMap(capturedEnvironmentOptionNames, q),
+ 'and',
+ )}`,
+ );
+ }
+
// Assert that only supported options were passed.
// Use Reflect.ownKeys to reject symbol-named properties as well.
const extraOptionsNames = ownKeys(extraOptions);
@@ -260,6 +294,7 @@ export const repairIntrinsics = (
optGetStackString = intrinsics['%InitialGetStackString%'];
}
const consoleRecord = tameConsole(
+ // @ts-ignore tameConsole does its own input validation
consoleTaming,
errorTrapping,
optGetStackString,
@@ -315,6 +350,7 @@ export const repairIntrinsics = (
// 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
enablePropertyOverrides(intrinsics, overrideTaming, overrideDebug);
if (__allowUnsafeMonkeyPatching__ !== 'unsafe') {

feat(ses): Read lockdown options from environment (#871)

*BREAKING CHANGE*: This may break applications that depend on lockdown
being idempotent, being able to call it multiple times.

Co-authored-by: Kris Kowal <kris@agoric.com>