feat: add evalTaming option
MODIFY
packages/ses/docs/lockdown.md
packages/ses/docs/lockdown.md
@@ -27,6 +27,7 @@ Each option is explained in its own section below.
| `consoleTaming` | `'safe'` | `'unsafe'` | deep stacks |
| `errorTaming` | `'safe'` | `'unsafe'` | `errorInstance.stack` |
| `errorTrapping` | `'platform'` | `'exit'` `'abort'` `'report'` | handling of uncaught exceptions |
+| `evalTaming` | `'safeEval'` | `'unsafeEval'` `'noEval'` | `eval` and `Function` of the start compartment. |
| `stackFiltering` | `'concise'` | `'verbose'` | deep stacks signal/noise |
| `overrideTaming` | `'moderate'` | `'min'` or `'severe'` | override mistake antidote |
| `overrideDebug` | `[]` | array of property names | detect override mistake |
@@ -366,12 +367,65 @@ the container to exit explicitly, and we highly recommend setting
- `'none'`: do not install traps for uncaught exceptions. Errors are likely to
appear as `{}` when they are reported by the default trap.
+## `evalTaming` Options
+
+This option only affects the start compartment!
+
+To disallows eval in the explicit compartments, replace the constructors in the compartment.
+
+```js
+const c = new Compartment()
+c.globalThis.eval = c.globalThis.Function = function() {
+ throw new TypeError()
+}
+```
+
+**Background**: Every realm has an implicit initial compartment we call the "start compartment". Explicit compartments are made with the `Compartment` constructor.
+For every compartment including the start compartment, there are evaluators `eval` and `Function`.
+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 (`"safeEval"`) 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 (direct-eval 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 `"unsafeEval"` 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`,
+like a development-mode bundling systems that use `eval`
+(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 `"noEval"` option emulates a Content Security Policy that disallows
+`unsafe-eval` by replacing all evaluators with functions that throw an
+exception.
+
+```js
+lockdown(); // evalTaming defaults to 'safeEval'
+// or
+lockdown({ evalTaming: 'noEval' }); // disallowing calling eval like there is a CSP limitation.
+// or
+
+// Please use this option with caution.
+// You may want to use Trusted Types or Content Security Policy with this option.
+lockdown({ evalTaming: 'unsafeEval' });
+```
+
## `stackFiltering` Options
**Background**: The error stacks shown by many JavaScript engines are
voluminous.
They contain many stack frames of functions in the infrastructure, that is
-usually irrelevant to the programmer trying to disagnose a bug. The SES-shim's
+usually irrelevant to the programmer trying to diagnose a bug. The SES-shim's
`console`, under the default `consoleTaming` option of `'safe'`, is even more
voluminous&mdash;displaying "deep stack" traces, tracing back through the
[eventually sent messages](https://github.com/tc39/proposal-eventual-send)

ADD
packages/ses/error-codes/SES_NO_EVAL.md
+# SES is disallowing eval in the current compartment (`SES_NO_EVAL`)
+
+The SES Hardened JavaScript shim is configured to reject any source evaluation in the current compartment. This is configured in the `lockdown` option. To mitigate this error, change the [lockdown option `"evalTaming"`](../docs/lockdown.md) from `"noEval"` to either `"safeEval"` (default) or `"unsafeEval"`.

MODIFY
packages/ses/index.d.ts
packages/ses/index.d.ts
@@ -23,6 +23,7 @@ export interface LockdownOptions {
errorTaming?: 'safe' | 'unsafe';
dateTaming?: 'safe' | 'unsafe'; // deprecated
mathTaming?: 'safe' | 'unsafe'; // deprecated
+ evalTaming?: 'safeEval' | 'unsafeEval' | 'noEval';
stackFiltering?: 'concise' | 'verbose';
overrideTaming?: 'moderate' | 'min' | 'severe';
overrideDebug?: Array<string>;

MODIFY
packages/ses/src/commons.js
packages/ses/src/commons.js
@@ -272,3 +272,9 @@ export const FERAL_EVAL = eval;
// Sample at module initialization time, which is before lockdown can
// repair it. Use it only to build powerless abstractions.
export const FERAL_FUNCTION = Function;
+
+export const noEvalEvaluate = () => {
+ throw new TypeError(
+ 'Cannot eval with evalTaming set to "noEval" (SES_NO_EVAL)',
+ );
+};

MODIFY
packages/ses/src/compartment-shim.js
packages/ses/src/compartment-shim.js
@@ -23,6 +23,7 @@ import {
import {
setGlobalObjectConstantProperties,
setGlobalObjectMutableProperties,
+ setGlobalObjectEvaluators,
} from './global-object.js';
import { isValidIdentifierName } from './scope-constants.js';
import { sharedGlobalPropertyNames } from './whitelist.js';
@@ -292,10 +293,16 @@ export const makeCompartmentConstructor = (
intrinsics,
newGlobalPropertyNames: sharedGlobalPropertyNames,
makeCompartmentConstructor: targetMakeCompartmentConstructor,
- safeEvaluate,
markVirtualizedNativeFunction,
});
+ // TODO: maybe add evalTaming to the Compartment constructor 3rd options?
+ setGlobalObjectEvaluators(
+ globalObject,
+ safeEvaluate,
+ markVirtualizedNativeFunction,
+ );
+
assign(globalObject, endowments);
weakmapSet(privateFields, this, {

MODIFY
packages/ses/src/global-object.js
packages/ses/src/global-object.js
@@ -34,7 +34,6 @@ export const setGlobalObjectConstantProperties = globalObject => {
* @param {Object} param1.intrinsics
* @param {Object} param1.newGlobalPropertyNames
* @param {Function} param1.makeCompartmentConstructor
- * @param {(string, Object?) => any} param1.safeEvaluate
* @param {(Object) => void} param1.markVirtualizedNativeFunction
*/
export const setGlobalObjectMutableProperties = (
@@ -43,7 +42,6 @@ export const setGlobalObjectMutableProperties = (
intrinsics,
newGlobalPropertyNames,
makeCompartmentConstructor,
- safeEvaluate,
markVirtualizedNativeFunction,
},
) => {
@@ -71,8 +69,6 @@ export const setGlobalObjectMutableProperties = (
const perCompartmentGlobals = {
globalThis: globalObject,
- eval: makeEvalFunction(safeEvaluate),
- Function: makeFunctionConstructor(safeEvaluate),
};
perCompartmentGlobals.Compartment = makeCompartmentConstructor(
@@ -95,3 +91,38 @@ export const setGlobalObjectMutableProperties = (
}
}
};
+
+/**
+ * setGlobalObjectEvaluators()
+ * Set the eval and the Function evaluator on the global object with given evalTaming policy.
+ *
+ * @param {Object} globalObject
+ * @param {Function} evaluator
+ * @param {(Object) => void} markVirtualizedNativeFunction
+ */
+export const setGlobalObjectEvaluators = (
+ globalObject,
+ evaluator,
+ markVirtualizedNativeFunction,
+) => {
+ {
+ const f = makeEvalFunction(evaluator);
+ markVirtualizedNativeFunction(f);
+ defineProperty(globalObject, 'eval', {
+ value: f,
+ writable: true,
+ enumerable: false,
+ configurable: true,
+ });
+ }
+ {
+ const f = makeFunctionConstructor(evaluator);
+ markVirtualizedNativeFunction(f);
+ defineProperty(globalObject, 'Function', {
+ value: f,
+ writable: true,
+ enumerable: false,
+ configurable: true,
+ });
+ }
+};

MODIFY
packages/ses/src/lockdown-shim.js
packages/ses/src/lockdown-shim.js
@@ -24,6 +24,7 @@ import {
is,
ownKeys,
stringSplit,
+ noEvalEvaluate,
} from './commons.js';
import { enJoin } from './error/stringify-utils.js';
import { makeHardener } from './make-hardener.js';
@@ -38,6 +39,7 @@ 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 './whitelist.js';
@@ -164,6 +166,7 @@ export const repairIntrinsics = (options = {}) => {
overrideTaming = 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 */
@@ -189,6 +192,13 @@ export const repairIntrinsics = (options = {}) => {
);
}
+ assert(
+ evalTaming === 'unsafeEval' ||
+ evalTaming === 'safeEval' ||
+ evalTaming === 'noEval',
+ d`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);
@@ -324,16 +334,32 @@ export const repairIntrinsics = (options = {}) => {
setGlobalObjectConstantProperties(globalThis);
- const { safeEvaluate } = makeSafeEvaluator({ globalObject: globalThis });
-
setGlobalObjectMutableProperties(globalThis, {
intrinsics,
newGlobalPropertyNames: initialGlobalPropertyNames,
makeCompartmentConstructor,
- safeEvaluate,
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.
*

MODIFY
packages/ses/src/make-eval-function.js
packages/ses/src/make-eval-function.js
-/*
+/**
* makeEvalFunction()
* A safe version of the native eval function which relies on
* the safety of safeEvaluate for confinement.
+ *
+ * @param {Function} safeEvaluate
*/
export const makeEvalFunction = safeEvaluate => {
// We use the the concise method syntax to create an eval without a

ADD
packages/ses/test/test-evalTaming-default.js
+import test from 'ava';
+import '../index.js';
+
+lockdown({});
+
+test('safe eval when evalTaming is undefined.', t => {
+ // eslint-disable-next-line no-unused-vars
+ const a = 0;
+ // eslint-disable-next-line no-eval
+ t.throws(() => eval('a'));
+ // eslint-disable-next-line no-eval
+ t.is(eval('1 + 1'), 2);
+
+ // should not throw
+ const compartment = new Compartment();
+ compartment.evaluate('(1, eval)("1 + 1")');
+ // eslint-disable-next-line no-eval
+ t.is(eval.toString(), 'function eval() { [native code] }');
+});

ADD
packages/ses/test/test-evalTaming-noEval.js
+import test from 'ava';
+import '../index.js';
+
+lockdown({ evalTaming: 'noEval' });
+
+test('no eval when evalTaming is noEval.', t => {
+ // eslint-disable-next-line no-eval
+ t.throws(() => eval('1+1'));
+
+ const compartment = new Compartment();
+ // should not throw
+ compartment.evaluate('(1, eval)("1 + 1")');
+ // eslint-disable-next-line no-eval
+ t.is(eval.toString(), 'function eval() { [native code] }');
+});

ADD
packages/ses/test/test-evalTaming-safeEval.js
+import test from 'ava';
+import '../index.js';
+
+lockdown({ evalTaming: 'safeEval' });
+
+test('safe eval when evalTaming is safeEval.', t => {
+ // eslint-disable-next-line no-unused-vars
+ const a = 0;
+ // eslint-disable-next-line no-eval
+ t.throws(() => eval('a'));
+ // eslint-disable-next-line no-eval
+ t.is(eval('1 + 1'), 2);
+ // eslint-disable-next-line no-eval
+ t.is(eval.toString(), 'function eval() { [native code] }');
+});

ADD
packages/ses/test/test-evalTaming-unsafe.js
+import test from 'ava';
+import '../index.js';
+
+lockdown({ evalTaming: 'unsafeEval' });
+
+test('direct eval is possible when evalTaming is unsafe.', t => {
+ // eslint-disable-next-line no-unused-vars
+ const a = 0;
+ // eslint-disable-next-line no-eval
+ t.is(eval('a'), 0);
+
+ // should not throw
+ const compartment = new Compartment();
+ compartment.evaluate('(1, eval)("1 + 1")');
+ // eslint-disable-next-line no-eval
+ t.is(eval.toString(), 'function eval() { [native code] }');
+});

MODIFY
packages/ses/test/test-global-object.js
packages/ses/test/test-global-object.js
@@ -6,6 +6,7 @@ import test from 'ava';
import {
setGlobalObjectConstantProperties,
setGlobalObjectMutableProperties,
+ setGlobalObjectEvaluators,
} from '../src/global-object.js';
import { sharedGlobalPropertyNames } from '../src/whitelist.js';
import { makeCompartmentConstructor } from '../src/compartment-shim.js';
@@ -27,9 +28,13 @@ test('globalObject', t => {
intrinsics,
newGlobalPropertyNames: sharedGlobalPropertyNames,
makeCompartmentConstructor,
- safeEvaluate,
markVirtualizedNativeFunction,
});
+ setGlobalObjectEvaluators(
+ globalObject,
+ safeEvaluate,
+ markVirtualizedNativeFunction,
+ );
t.truthy(globalObject instanceof Object);
t.is(Object.getPrototypeOf(globalObject), Object.prototype);

feat: add evalTaming option (#961)