feat(ses): Create a thin lockdown layer
MODIFY
packages/ses/NEWS.md
packages/ses/NEWS.md
@@ -2,6 +2,9 @@ User-visible changes in SES:
## Next release
+* Creates a `ses/lockdown` module that only introduces `lockdown` and `harden`
+ to global scope, for a much smaller payload than `ses`, which entrains a
+ JavaScript parser to support ECMAScript modules.
* Adds the `load` method to `Compartment`.
Load allows a bundler or archiver to use the `Compartment` API to gather the
transitive dependencies of modules without executing them.

MODIFY
packages/ses/package.json
packages/ses/package.json
"import": "./src/main.js",
"require": "./dist/ses.cjs",
"browser": "./dist/ses.umd.js"
+ },
+ "./lockdown": {
+ "import": "./src/lockdown-main.js",
+ "require": "./dist/lockdown.cjs",
+ "browser": "./dist/lockdown.umd.js"
}
},
"scripts": {

MODIFY
packages/ses/rollup.config.js
packages/ses/rollup.config.js
@@ -23,6 +23,16 @@ export default [
],
plugins: [resolve(), commonjs()],
},
+ {
+ input: 'src/lockdown-main.js',
+ output: [
+ {
+ file: `dist/lockdown.cjs`,
+ format: 'cjs',
+ },
+ ],
+ plugins: [resolve(), commonjs()],
+ },
{
input: 'src/main.js',
output: {
@@ -32,6 +42,15 @@ export default [
},
plugins: [resolve(), commonjs()],
},
+ {
+ input: 'src/lockdown-main.js',
+ output: {
+ file: `dist/lockdown.umd.js`,
+ format: 'umd',
+ name: umd,
+ },
+ plugins: [resolve(), commonjs()],
+ },
{
input: 'src/main.js',
output: {
@@ -41,4 +60,13 @@ export default [
},
plugins: [resolve(), commonjs(), terser()],
},
+ {
+ input: 'src/lockdown-main.js',
+ output: {
+ file: `dist/lockdown.umd.min.js`,
+ format: 'umd',
+ name: umd,
+ },
+ plugins: [resolve(), commonjs(), terser()],
+ },
];

MODIFY
packages/ses/src/compartment-shim.js
packages/ses/src/compartment-shim.js
@@ -21,6 +21,7 @@ import { isValidIdentifierName } from './scope-constants.js';
import { sharedGlobalPropertyNames } from './whitelist.js';
import { getGlobalIntrinsics } from './intrinsics.js';
import { tameFunctionToString } from './tame-function-tostring.js';
+import { InertCompartment, InertStaticModuleRecord } from './inert.js';
// q, for quoting strings.
const q = JSON.stringify;
@@ -52,25 +53,6 @@ export function StaticModuleRecord(string, url) {
moduleAnalyses.set(this, analysis);
}
-// It is not clear that
-// `StaticModuleRecord.prototype.constructor` needs to be the
-// useless `InertStaticModuleRecord` rather than
-// `StaticModuleRecord` itself. The reason we're starting off
-// extra caution is that `StaticModuleRecord` would be the only
-// remaining universally shared primordial reachable by navigation
-// that can turn strings of code into a representation closer to
-// code execution. The others, `eval`, `Function`, and `Compartment`
-// are already protected, and only `Compartment` can turn a
-// `StaticModuleRecord` into execution, which is why it would
-// probably be safe. However, since this extra caution has a tiny
-// cost, I'd rather start out more restrictive, maintaining the option
-// to loosen the rule over time.
-//
-// eslint-disable-next-line no-shadow
-const InertStaticModuleRecord = function StaticModuleRecord(_string, _url) {
- throw new TypeError('Not available');
-};
-
const StaticModuleRecordPrototype = {
constructor: InertStaticModuleRecord,
toString() {
@@ -110,14 +92,6 @@ const assertModuleHooks = compartment => {
}
};
-const InertCompartment = function Compartment(
- _endowments = {},
- _modules = {},
- _options = {},
-) {
- throw new TypeError('Not available');
-};
-
const CompartmentPrototype = {
constructor: InertCompartment,
get globalThis() {

MODIFY
packages/ses/src/get-anonymous-intrinsics.js
packages/ses/src/get-anonymous-intrinsics.js
import { getOwnPropertyDescriptor, getPrototypeOf } from './commons.js';
-import { Compartment, StaticModuleRecord } from './compartment-shim.js';
+import { InertCompartment, InertStaticModuleRecord } from './inert.js';
/**
* Object.getConstructorOf()
@@ -90,9 +90,6 @@ export function getAnonymousIntrinsics() {
async function AsyncFunctionInstance() {}
const AsyncFunction = getConstructorOf(AsyncFunctionInstance);
- const InertCompartment = Compartment.prototype.constructor;
- const InertStaticModuleRecord = StaticModuleRecord.prototype.constructor;
-
const intrinsics = {
'%InertFunction%': InertFunction,
'%ArrayIteratorPrototype%': ArrayIteratorPrototype,

ADD
packages/ses/src/inert.js
+export const InertCompartment = function Compartment(
+ _endowments = {},
+ _modules = {},
+ _options = {},
+) {
+ throw new TypeError('Not available');
+};
+
+// It is not clear that
+// `StaticModuleRecord.prototype.constructor` needs to be the
+// useless `InertStaticModuleRecord` rather than
+// `StaticModuleRecord` itself. The reason we're starting off
+// extra caution is that `StaticModuleRecord` would be the only
+// remaining universally shared primordial reachable by navigation
+// that can turn strings of code into a representation closer to
+// code execution. The others, `eval`, `Function`, and `Compartment`
+// are already protected, and only `Compartment` can turn a
+// `StaticModuleRecord` into execution, which is why it would
+// probably be safe. However, since this extra caution has a tiny
+// cost, I'd rather start out more restrictive, maintaining the option
+// to loosen the rule over time.
+//
+export const InertStaticModuleRecord = function StaticModuleRecord(
+ _string,
+ _url,
+) {
+ throw new TypeError('Not available');
+};

ADD
packages/ses/src/lockdown-main.js
+import { assign } from './commons.js';
+import { makeLockdown, harden } from './lockdown-shim.js';
+
+assign(globalThis, {
+ harden,
+ lockdown: makeLockdown(),
+});

MODIFY
packages/ses/src/lockdown-shim.js
packages/ses/src/lockdown-shim.js
@@ -31,7 +31,6 @@ import { getAnonymousIntrinsics } from './get-anonymous-intrinsics.js';
import { initGlobalObject } from './global-object.js';
import { initialGlobalPropertyNames } from './whitelist.js';
import { tameFunctionToString } from './tame-function-tostring.js';
-import { makeCompartmentConstructor } from './compartment-shim.js';
let firstOptions;
@@ -50,7 +49,7 @@ export const harden = ref => {
const alreadyHardenedIntrinsics = () => false;
-export function repairIntrinsics(options = {}) {
+export function repairIntrinsics(makeCompartmentConstructor, options = {}) {
// First time, absent options default to 'safe'.
// Subsequent times, absent options default to first options.
// Thus, all present options must agree with first options.
@@ -164,7 +163,13 @@ export function repairIntrinsics(options = {}) {
return hardenIntrinsics;
}
-export const lockdown = (options = {}) => {
- const maybeHardenIntrinsics = repairIntrinsics(options);
+export const makeLockdown = (makeCompartmentConstructor = undefined) => {
+ const lockdown = (options = {}) => {
+ const maybeHardenIntrinsics = repairIntrinsics(
+ makeCompartmentConstructor,
+ options,
+ );
return maybeHardenIntrinsics();
};
+ return lockdown;
+};

MODIFY
packages/ses/src/main.js
packages/ses/src/main.js
// See the License for the specific language governing permissions and
// limitations under the License.
-import { lockdown, harden } from './lockdown-shim.js';
-import { Compartment, StaticModuleRecord } from './compartment-shim.js';
+// Importing the lower-layer "./lockdown.js" ensures that we run later and
+// replace its global lockdown if an application elects to import both.
+import './lockdown-main.js';
+import { assign } from './commons.js';
+import { makeLockdown } from './lockdown-shim.js';
+import {
+ makeCompartmentConstructor,
+ Compartment,
+ StaticModuleRecord,
+} from './compartment-shim.js';
-Object.assign(globalThis, {
- lockdown,
- harden,
+assign(globalThis, {
+ lockdown: makeLockdown(makeCompartmentConstructor),
Compartment,
StaticModuleRecord,
});

MODIFY
packages/ses/test/lockdown-options.test.js
packages/ses/test/lockdown-options.test.js
import test from 'tape';
-import { lockdown } from '../src/lockdown-shim.js';
+import { makeLockdown } from '../src/lockdown-shim.js';
+
+const lockdown = makeLockdown();
test('lockdown throws with non-recognized options', t => {
t.plan(2);

feat(ses): Create a thin lockdown layer (#406)