Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/ses/NEWS.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
43 changes: 34 additions & 9 deletions packages/ses/docs/lockdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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.

* <https://github.com/facebook/hermes/issues/957>
* <https://github.com/facebook/hermes/pull/1515>

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:
<https://github.com/leotm/hermes/tree/ses-lockdown-test-static-hermes-compiler-vm>

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.

* <https://github.com/facebook/hermes/issues/1056>
* <https://github.com/facebook/hermes/pull/1571>

There is also an open alternate idea to sandbox `Compartment` *without* the `with` statement.

* <https://github.com/endojs/endo/discussions/1944>

If `lockdown` does not receive an `evalTaming` option, it will respect
`process.env.LOCKDOWN_EVAL_TAMING`.

Expand Down
8 changes: 3 additions & 5 deletions packages/ses/scripts/hermes-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
63 changes: 43 additions & 20 deletions packages/ses/src/lockdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Comment on lines +108 to +111
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't 'no-eval' also disallow these?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where we check what the capabilities of the host are.
no-eval should have an effect after this check.

If the above is not correct, we have to look into running this earlier or changing the logic of asserting on the results of probeHostEvaluators a bit.

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',
`\
Expand All @@ -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 };
};

/**
Expand All @@ -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.
Expand Down Expand Up @@ -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.`,
);
Expand All @@ -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
Expand Down
9 changes: 5 additions & 4 deletions packages/ses/src/make-eval-function.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,7 +20,7 @@ export const makeEvalFunction = safeEvaluate => {
// rule. Track.
return source;
}
return safeEvaluate(source);
return evaluator(source);
},
}.eval;

Expand Down
9 changes: 5 additions & 4 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 `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.
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 All @@ -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;
};
17 changes: 16 additions & 1 deletion packages/ses/test/_hermes-smoke.js
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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',
);