diff --git a/packages/ses/NEWS.md b/packages/ses/NEWS.md index c567019034..050d41535c 100644 --- a/packages/ses/NEWS.md +++ b/packages/ses/NEWS.md @@ -1,4 +1,10 @@ -User-visible changes in `ses`: +User-visible changes in `ses` + +# Next release + +- The `evalTaming:` option `safe-eval` now can only throw error `SES_DIRECT_EVAL`. + This allows SES to initialize with `'unsafe-eval'` or `no-eval` on hosts with no + direct eval available (e.g. Hermes) for a successful lockdown. # v1.12.0 (2025-03-11) diff --git a/packages/ses/docs/lockdown.md b/packages/ses/docs/lockdown.md index 125b707fa7..f608622aa5 100644 --- a/packages/ses/docs/lockdown.md +++ b/packages/ses/docs/lockdown.md @@ -463,7 +463,7 @@ the container to exit explicitly, and we highly recommend setting ## `reporting` Options -**Background**: Lockdown and `repairIntrinsics` report warnings if they +**Background**: `lockdown` and `repairIntrinsics` report warnings if they encounter unexpected but repairable variations on the shared intrinsics, which regularly occurs if the version of `ses` predates the introduction of new language features. @@ -574,27 +574,27 @@ The default lockdown behavior isolates all of these evaluators. Replacing the realm's initial evaluators is not necessary to ensure the isolation of guest code because guest code must not run in the start compartment. -Although the code run in the start compartment is normally referred to as "trusted", we mean only that we assume it was not written maliciously. It may still be buggy, and it may be buggy in a way that is exploitable by malicious guest code. To limit the harm that such vulnerabilities can cause, the default (`"safe-eval"`) setting replaces the evaluators of the start compartment with their safe alternatives. +Although the code run in the start compartment is normally referred to as "trusted", we mean only that we assume it was not written maliciously. It may still be buggy, and it may be buggy in a way that is exploitable by malicious guest code. To limit the harm that such vulnerabilities can cause, the default (`'safe-eval'`) setting replaces the evaluators of the start compartment with their safe alternatives. However, in the shim, only the exact `eval` function from the start compartment can be used to -perform direct-eval, which runs in the lexical scope in which the direct-eval syntax appears (the direct-eval syntax is a special form rather than a function call). -The SES shim itself uses direct-eval internally to construct an isolated +perform direct eval, which runs in the lexical scope in which the direct eval syntax appears (the direct eval syntax is a special form rather than a function call). +The SES shim itself uses direct eval internally to construct an isolated evaluator, so replacing the initial `eval` prevents any subsequent program from using the same mechanism to isolate a guest program. -The `"unsafe-eval"` option for `evalTaming` leaves the original `eval` in place +The `'unsafe-eval'` option for `evalTaming` leaves the original `eval` in place for other isolation mechanisms like isolation code generators that work in tandem with SES. -This option may be useful for web pages with an environment that allows `unsafe-eval`, +This option may be useful for web pages with an environment that allows `'unsafe-eval'`, like a development-mode bundling systems that use `eval` -(for example, [`"eval-source-map"` in webpack](https://webpack.js.org/configuration/devtool/#devtool)). +(for example, [`'eval-source-map'` in webpack](https://webpack.js.org/configuration/devtool/#devtool)). In these cases, SES cannot be responsible for maintaining the isolation of guest code. If you're going to use `eval`, [Trusted Types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types) may help maintain security. -The `"no-eval"` option emulates a Content Security Policy that disallows -`unsafe-eval` by replacing all evaluators with functions that throw an +The `'no-eval'` option emulates a Content Security Policy that disallows +`'unsafe-eval'` by replacing all evaluators with functions that throw an exception. ```js @@ -608,6 +608,31 @@ lockdown({ evalTaming: 'no-eval' }); // disallowing calling eval like there is a lockdown({ evalTaming: 'unsafe-eval' }); ``` +Also `'unsafe-eval'` and `no-eval` allow us to initialize SES when no direct eval is available. + +**Background**: Hermes is a JavaScript engine that does not yet support direct `eval()` nor the `with` statement. Default option `'safe-eval'` evaluates the source using direct eval and multiple nested `with` statements to create a restricted scope chain that constructs the isolated evaluator. This leaves us with options `'unsafe-eval'` or `'no-eval'`. + +Note: In the future when the Compartment global class is supported on Hermes after `lockdown`, attempting to evaluate a compartment will emit on Hermes `Uncaught SyntaxError: 2:5:invalid statement encountered` (referring to make-evaluate.js > evaluateFactory) if the `with` statement is still unsupported. + +Once Hermes engine supports direct eval, the `SES_DIRECT_EVAL` error will not longer prevent SES initializing with `'safe-eval'`. +Currently there is an open feature request and open pull request targeting Static Hermes. + +* + * + +You can also test and verify `lockdown` completing on this change by [building and running](https://github.com/facebook/hermes/blob/static_h/doc/BuildingAndRunning.md) Static Hermes on the following fork for example: + + +Once Hermes engine supports direct eval and the `with` statement, `'safe-eval'` will work. +Currently there is an open feature request and open pull request targeting Static Hermes. + +* + * + +There is also an open alternate idea to sandbox `Compartment` *without* the `with` statement. + +* + If `lockdown` does not receive an `evalTaming` option, it will respect `process.env.LOCKDOWN_EVAL_TAMING`. diff --git a/packages/ses/scripts/hermes-test.sh b/packages/ses/scripts/hermes-test.sh index 2fdfa1688b..dbd0b379d2 100755 --- a/packages/ses/scripts/hermes-test.sh +++ b/packages/ses/scripts/hermes-test.sh @@ -41,11 +41,9 @@ $HERMESC test/_hermes-smoke-dist.js -emit-binary -out test/_hermes-smoke-dist.hb echo "Generated: test/_hermes-smoke-dist.hbc" echo "Hermes compiler done" -# TODO: Disabled until https://github.com/endojs/endo/issues/1891 complete -# echo "Executing generated bytecode file on Hermes VM" -# $HERMES -b test/_hermes-smoke-dist.hbc -# echo "Hermes VM done" -echo "Skipping: Hermes VM" +echo "Executing generated bytecode file on Hermes VM" +$HERMES -b test/_hermes-smoke-dist.hbc +echo "Hermes VM done" echo "Hermes tests complete" diff --git a/packages/ses/src/lockdown.js b/packages/ses/src/lockdown.js index 993ca1631b..21bac4da6f 100644 --- a/packages/ses/src/lockdown.js +++ b/packages/ses/src/lockdown.js @@ -100,10 +100,32 @@ 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 probeHostEvaluators = () => { + let functionAllowed; try { - allowed = FERAL_FUNCTION( + functionAllowed = FERAL_FUNCTION('return true')(); + } catch (_error) { + // We reach here if the Function() constructor is outright forbidden by a + // strict Content Security Policy (containing either a `default-src` or a + // `script-src` directive), not been implemented in the host, or the host + // is configured to throw an exception instead of `new Function`. + functionAllowed = false; + } + + let evalAllowed; + try { + evalAllowed = FERAL_EVAL('true'); + } catch (_error) { + // We reach here if `eval` is outright forbidden by a strict Content Security Policy, + // not implemented in the host, or the host is configured to throw an exception. + // We allow this for SES usage that delegates the responsibility to isolate + // guest code to production code generation. + evalAllowed = false; + } + + let directEvalAllowed; + if (functionAllowed && evalAllowed) { + directEvalAllowed = FERAL_FUNCTION( 'eval', 'SES_changed', `\ @@ -115,21 +137,12 @@ 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 (!directEvalAllowed) { 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)`, - ); } + + return { functionAllowed, evalAllowed, directEvalAllowed }; }; /** @@ -152,11 +165,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. @@ -221,13 +234,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.`, ); @@ -245,7 +256,19 @@ export const repairIntrinsics = (options = {}) => { // trace retained: priorRepairIntrinsics.stack; - assertDirectEvalAvailable(); + const { functionAllowed, evalAllowed, directEvalAllowed } = + probeHostEvaluators(); + + if ( + directEvalAllowed === false && + evalTaming === 'safe-eval' && + (functionAllowed || evalAllowed) + ) { + // 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)", + ); + } /** * Because of packagers and bundlers, etc, multiple invocations of lockdown diff --git a/packages/ses/src/make-eval-function.js b/packages/ses/src/make-eval-function.js index 1c3e77600f..4d0a522429 100644 --- a/packages/ses/src/make-eval-function.js +++ b/packages/ses/src/make-eval-function.js @@ -1,11 +1,12 @@ /** * makeEvalFunction() * A safe version of the native eval function which relies on - * the safety of safeEvaluate for confinement. + * the safety of `safe-eval` for confinement, unless `no-eval` + * is specified (then a TypeError is thrown on use). * - * @param {Function} safeEvaluate + * @param {Function} evaluator */ -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 @@ -19,7 +20,7 @@ export const makeEvalFunction = safeEvaluate => { // rule. Track. return source; } - return safeEvaluate(source); + return evaluator(source); }, }.eval; diff --git a/packages/ses/src/make-function-constructor.js b/packages/ses/src/make-function-constructor.js index 47b86ef794..c94485a15f 100644 --- a/packages/ses/src/make-function-constructor.js +++ b/packages/ses/src/make-function-constructor.js @@ -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 `safe-eval` for confinement, unless `no-eval` + * is specified (then a TypeError is thrown on use). */ -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. @@ -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, { @@ -72,7 +73,7 @@ export const makeFunctionConstructor = safeEvaluate => { getPrototypeOf(FERAL_FUNCTION) === FERAL_FUNCTION.prototype || Fail`Function prototype is the same accross compartments`; getPrototypeOf(newFunction) === FERAL_FUNCTION.prototype || - Fail`Function constructor prototype is the same accross compartments`; + Fail`Function constructor prototype is the same across compartments`; return newFunction; }; diff --git a/packages/ses/test/_hermes-smoke.js b/packages/ses/test/_hermes-smoke.js index 553a732521..aadaea3d8a 100644 --- a/packages/ses/test/_hermes-smoke.js +++ b/packages/ses/test/_hermes-smoke.js @@ -1,3 +1,5 @@ +/* eslint-disable no-eval */ + // Hermes doesn't support native I/O, // so we concat the SES shim above, // when running this test on Hermes. @@ -6,7 +8,20 @@ * Test calling SES lockdown. */ const testLockdown = () => { - lockdown(); + lockdown({ evalTaming: 'unsafe-eval' }); }; testLockdown(); + +assert(typeof eval === 'function', 'eval is not a function'); +assert( + eval.toString() === 'function eval() { [native code] }', + 'eval is not a native code function', +); +assert(eval(42) === 42, 'eval is not functional'); +assert(eval('42') === 42, 'eval called with string argument is not functional'); +assert( + // eslint-disable-next-line no-new-func + Function('return 42')() === 42, + 'Function constructor is not functional', +);