Skip to content

Commit

Permalink
feat(ses): hostEvaluators lockdown option
Browse files Browse the repository at this point in the history
  • Loading branch information
leotm committed Feb 26, 2025
1 parent fd7ad98 commit 88ddc37
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 36 deletions.
4 changes: 4 additions & 0 deletions packages/ses/docs/lockdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ If it does, then we may decide to support them, but *only* under the
compartment. The `RegExp` constructor shared by other compartments will remain
safe and powerless.

## `hostEvaluators` Options

TODO: Introduced for Hermes.

## `localeTaming` Options

**Background**: In standard plain JavaScript, the builtin methods with
Expand Down
14 changes: 13 additions & 1 deletion packages/ses/error-codes/SES_DIRECT_EVAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,19 @@
The SES Hardened JavaScript shim captures the `eval` function when it is
initialized.
The `eval` function it finds must be the original `eval` because SES uses its
dynamic scope to implement its isolated eval.
dynamic scope to implement its isolated eval.

If you see this error, something running before `ses` initialized, most likely
another instance of `ses`, has replaced `eval` with something else.

If you're running under an environment that doesn't support direct eval (Hermes), try setting `hostEvaluators` to `no-direct`.

If you're running under CSP, try setting it to `none`?

# _hostEvaluators_ was set to _none_, but evaluators are not blocked (`SES_DIRECT_EVAL`)

It seems your CSP has no effect.

# `"hostEvaluators" was set to "no-direct", but

If evaluators are working, if seems you've upgraded host to a version that now supports them (future Hermes).
2 changes: 1 addition & 1 deletion packages/ses/src/global-object.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { constantProperties, universalPropertyNames } from './permits.js';
* guest programs, we cannot emulate the proper behavior.
* With this shim, assigning Symbol.unscopables causes the given lexical
* names to fall through to the terminal scope proxy.
* But, we can install this setter to prevent a program from proceding on
* But, we can install this setter to prevent a program from proceeding on
* this false assumption.
*
* @param {object} globalObject
Expand Down
83 changes: 69 additions & 14 deletions packages/ses/src/lockdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,13 @@ const safeHarden = makeHardener();
// only ever need to be called once and that simplifying lockdown will improve
// the quality of audits.

const assertDirectEvalAvailable = () => {
let allowed = false;
const assertDirectEval = hostEvaluators => {
let evalAllowed = false;
let functionAllowed = false;
let evaluatorsSuccessful = true;
try {
allowed = FERAL_FUNCTION(
functionAllowed = FERAL_FUNCTION('return true')();
evalAllowed = FERAL_FUNCTION(
'eval',
'SES_changed',
`\
Expand All @@ -115,20 +118,53 @@ const assertDirectEvalAvailable = () => {
// 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) {
if (!evalAllowed) {
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;
evaluatorsSuccessful = false;
}
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)`,

const reporter =
/** @type {"platform" | "console" | "none"} */ chooseReporter(
// @ts-ignore
getenv('LOCKDOWN_REPORTING', 'platform'),
);
const { warn } = reporter;

switch (hostEvaluators) {
case 'all':
assert(
evalAllowed === true &&
functionAllowed === true &&
evaluatorsSuccessful === true,
"SES cannot initialize unless 'eval' is the original intrinsic 'eval', suitable for direct-eval (dynamically scoped eval) (SES_DIRECT_EVAL)",
);
break;
case 'none':
assert(
evalAllowed === false &&
functionAllowed === false &&
evaluatorsSuccessful === false,
'"hostEvaluators" was set to "none", but evaluators are not blocked (SES_DIRECT_EVAL)',
);
break;
case 'no-direct':
warn(
`SES initializing with sloppy and indirect 'eval', not suitable for direct-eval (dynamically scoped eval) (SES_DIRECT_EVAL)`,
);
assert(
evalAllowed === false &&
functionAllowed === true &&
evaluatorsSuccessful === true,
`"hostEvaluators" was set to "no-direct", but ${evalAllowed === true ? 'direct eval is functional' : 'evaluators are not allowed'} (SES_DIRECT_EVAL)`,
);
break;
default:
throw TypeError(`Invalid hostEvaluators option ${hostEvaluators}`);
}
};

Expand All @@ -152,11 +188,11 @@ export const repairIntrinsics = (options = {}) => {
// 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
// the raw stack frames that can be quite verbose. 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
// track down might be in the infrastructure, 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.
Expand Down Expand Up @@ -189,6 +225,11 @@ export const repairIntrinsics = (options = {}) => {
/** @param {string} debugName */
debugName => debugName !== '',
),
// TODO: Breaking change
// Backwards compatible change when hostEvaluators not provided
hostEvaluators = /** @type { 'all' | 'none' | 'no-direct' } */ (
getenv('LOCKDOWN_HOSTS_EVALUATORS', 'all')
),
legacyRegeneratorRuntimeTaming = getenv(
'LOCKDOWN_LEGACY_REGENERATOR_RUNTIME_TAMING',
'safe',
Expand All @@ -208,6 +249,15 @@ export const repairIntrinsics = (options = {}) => {
evalTaming === 'noEval' ||
Fail`lockdown(): non supported option evalTaming: ${q(evalTaming)}`;

hostEvaluators === 'all' ||
hostEvaluators === 'none' ||
hostEvaluators === 'no-direct' ||
Fail`lockdown(): non supported option hostEvaluators: ${q(hostEvaluators)}`;

evalTaming === 'safeEval' &&
hostEvaluators === 'no-direct' &&
Fail`lockdown(): option evalTaming: ${q(evalTaming)} is incompatible with hostEvaluators: ${q(hostEvaluators)}`;

// Assert that only supported options were passed.
// Use Reflect.ownKeys to reject symbol-named properties as well.
const extraOptionsNames = ownKeys(extraOptions);
Expand All @@ -218,13 +268,11 @@ export const repairIntrinsics = (options = {}) => {
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.`,
);
Expand All @@ -242,7 +290,7 @@ export const repairIntrinsics = (options = {}) => {
// trace retained:
priorRepairIntrinsics.stack;

assertDirectEvalAvailable();
assertDirectEval(hostEvaluators);

/**
* Because of packagers and bundlers, etc, multiple invocations of lockdown
Expand Down Expand Up @@ -408,6 +456,13 @@ export const repairIntrinsics = (options = {}) => {
markVirtualizedNativeFunction,
});

// TODO: disable compartmentInstance.evaluate instead?
if (hostEvaluators === 'no-direct') {
globalThis.testCompartmentHooks = undefined;
// @ts-ignore Compartment does exist on globalThis
delete globalThis.Compartment;
}

if (evalTaming === 'noEval') {
setGlobalObjectEvaluators(
globalThis,
Expand Down
10 changes: 6 additions & 4 deletions packages/ses/src/make-eval-function.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/**
* makeEvalFunction()
* A safe version of the native eval function which relies on
* the safety of safeEvaluate for confinement.
* the safety of safeEvaluate for confinement, unless noEval
* is specified (then a TypeError is thrown).
*
* @param {Function} safeEvaluate
* @param {Function} evaluator
* @param legacyHermesTaming

Check warning on line 8 in packages/ses/src/make-eval-function.js

View workflow job for this annotation

GitHub Actions / lint

@param "legacyHermesTaming" does not match an existing function parameter

Check warning on line 8 in packages/ses/src/make-eval-function.js

View workflow job for this annotation

GitHub Actions / lint

Missing JSDoc @param "legacyHermesTaming" type
*/
export const makeEvalFunction = safeEvaluate => {
export const makeEvalFunction = evaluator => {
// We use 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
Expand All @@ -19,7 +21,7 @@ export const makeEvalFunction = safeEvaluate => {
// rule. Track.
return source;
}
return safeEvaluate(source);
return evaluator(source);
},
}.eval;

Expand Down
7 changes: 4 additions & 3 deletions packages/ses/src/make-function-constructor.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ const { Fail } = assert;
/*
* makeFunctionConstructor()
* A safe version of the native Function which relies on
* the safety of safeEvaluate for confinement.
* the safety of safeEvaluate for confinement, unless noEval
* is specified (then a TypeError is thrown).
*/
export const makeFunctionConstructor = safeEvaluate => {
export const makeFunctionConstructor = evaluator => {
// Define an unused parameter to ensure Function.length === 1
const newFunction = function Function(_body) {
// Sanitize all parameters at the entry point.
Expand Down Expand Up @@ -54,7 +55,7 @@ export const makeFunctionConstructor = safeEvaluate => {
// 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 safeEvaluate(src);
return evaluator(src);
};

defineProperties(newFunction, {
Expand Down
44 changes: 31 additions & 13 deletions packages/ses/src/permits.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
/* eslint-disable no-restricted-globals */
/* eslint max-lines: 0 */

import { arrayPush, arrayForEach } from './commons.js';
import {
arrayPush,
arrayForEach,
getOwnPropertyDescriptor,
} from './commons.js';

/** @import {GenericErrorConstructor} from '../types.js' */

Expand Down Expand Up @@ -316,23 +320,37 @@ const accessor = {
set: fn,
};

// TODO Remove this once we no longer support Hermes.
// While all engines have a ThrowTypeError accessor for fields not permitted in strict mode,
// some (Hermes 0.12) put that accessor in unexpected places.
// We can't clean them up because they're non-configurable.
// Therefore we're checking for identity with specCompliantThrowTypeError and dynamically adding permits for those.

// eslint-disable-next-line func-names
const strict = function () {
const specCompliantThrowTypeError = (function () {
'use strict';
};

// TODO Remove this once we no longer support the Hermes that needed this.
arrayForEach(['caller', 'arguments'], prop => {
try {
strict[prop];
} catch (e) {
// https://github.com/facebook/hermes/blob/main/test/hermes/function-non-strict.js
if (e.message === 'Restricted in strict mode') {
// Fixed in Static Hermes: https://github.com/facebook/hermes/issues/1582
// eslint-disable-next-line prefer-rest-params
const desc = getOwnPropertyDescriptor(arguments, 'callee');
return desc && desc.get;
})();
if (specCompliantThrowTypeError) {
// eslint-disable-next-line func-names
const strict = function () {
'use strict';
};
arrayForEach(['caller', 'arguments'], prop => {
const desc = getOwnPropertyDescriptor(strict, prop);
if (
desc &&
desc.configurable === false &&
desc.get &&
desc.get === specCompliantThrowTypeError
) {
FunctionInstance[prop] = accessor;
}
}
});
});
}

export const isAccessorPermit = permit => {
return permit === getter || permit === accessor;
Expand Down
1 change: 1 addition & 0 deletions packages/ses/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface RepairOptions {
overrideTaming?: 'moderate' | 'min' | 'severe';
overrideDebug?: Array<string>;
domainTaming?: 'safe' | 'unsafe';
hostEvaluators?: 'all' | 'none' | 'unsafe';
/**
* safe (default): do nothing.
*
Expand Down

0 comments on commit 88ddc37

Please sign in to comment.