feat(ses): Allow import and eval methods
MODIFY
packages/compartment-mapper/src/import-archive.js
packages/compartment-mapper/src/import-archive.js
@@ -110,9 +110,7 @@ export const parseArchive = async (archiveBytes, archiveLocation) => {
__shimTransforms__,
Compartment,
});
- // Call import by property to bypass SES censoring for dynamic import.
- // eslint-disable-next-line dot-notation
- return compartment['import'](moduleSpecifier);
+ return compartment.import(moduleSpecifier);
};
return { import: execute };
@@ -136,7 +134,5 @@ export const loadArchive = async (read, archiveLocation) => {
*/
export const importArchive = async (read, archiveLocation, options) => {
const archive = await loadArchive(read, archiveLocation);
- // Call import by property to bypass SES censoring for dynamic import.
- // eslint-disable-next-line dot-notation
- return archive['import'](options);
+ return archive.import(options);
};

MODIFY
packages/compartment-mapper/src/import.js
packages/compartment-mapper/src/import.js
@@ -59,9 +59,7 @@ export const loadLocation = async (read, moduleLocation, options) => {
__shimTransforms__,
Compartment,
});
- // Call import by property to bypass SES censoring for dynamic import.
- // eslint-disable-next-line dot-notation
- return compartment['import'](moduleSpecifier);
+ return compartment.import(moduleSpecifier);
};
return { import: execute };
@@ -76,7 +74,5 @@ export const loadLocation = async (read, moduleLocation, options) => {
*/
export const importLocation = async (read, moduleLocation, options = {}) => {
const application = await loadLocation(read, moduleLocation, options);
- // Call import by property to bypass SES censoring for dynamic import.
- // eslint-disable-next-line dot-notation
- return application['import'](options);
+ return application.import(options);
};

MODIFY
packages/ses/NEWS.md
packages/ses/NEWS.md
@@ -2,7 +2,9 @@ User-visible changes in SES:
## Next Release
-No changes yet.
+- Relaxes the censorship of `import` and `eval` in programs evaluated
+ under SES to specifically allow the use of `import.import()` or
+ `evaluator.eval()` methods.
## Release 0.12.7 (5-April-2021)

MODIFY
packages/ses/error-codes/SES_IMPORT_REJECTED.md
packages/ses/error-codes/SES_IMPORT_REJECTED.md
@@ -15,20 +15,10 @@ Moddable's XS machine, does not suffer from this limitation.
## Manually evading the import expression rejection
-Compartments implement a similar `import` method you could use instead. But to
-invoke it without tripping over the regular expression you need to call it as a
-computed property.
+Compartments implement a similar `import` method you could use instead.
```js
-compartment['import'](specifier);
-```
-
-Using parentheses could also work, but unnecessary parentheses are
-unlikely to survive a round trip through a minifier, so be sure to
-also use a comma expression.
-
-```js
-(0, compartment.import)(specifier);
+compartment.import(specifier);
```
However, beware the difference in semantics. The dynamic import expression is

MODIFY
packages/ses/src/transforms.js
packages/ses/src/transforms.js
@@ -16,7 +16,13 @@ function getLineNumber(src, pattern) {
if (index < 0) {
return -1;
}
- return stringSplit(stringSlice(src, 0, index), '\n').length;
+
+ // The importPattern incidentally captures an initial \n in
+ // an attempt to reject a . prefix, so we need to offset
+ // the line number in that case.
+ const adjustment = src[index] === '\n' ? 1 : 0;
+
+ return stringSplit(stringSlice(src, 0, index), '\n').length + adjustment;
}
// /////////////////////////////////////////////////////////////////////////////
@@ -90,7 +96,7 @@ export function evadeHtmlCommentTest(src) {
// /////////////////////////////////////////////////////////////////////////////
-const importPattern = new RegExp('\\bimport(\\s*(?:\\(|/[/*]))', 'g');
+const importPattern = new RegExp('(^|[^.])\\bimport(\\s*(?:\\(|/[/*]))', 'g');
/**
* Conservatively reject the source text if it may contain a dynamic
@@ -146,21 +152,17 @@ export function rejectImportExpressions(src) {
* the meaning of the program, depending on the binding, if any, of the lexical
* variable `__import__`.
*
- * Finally, if the original appears in code where it is not parsed as an
- * expression, for example `foo.import(path)`, then this evasion would rewrite
- * to `foo.__import__(path)` which has a surprisingly different meaning.
- *
* @param { string } src
* @returns { string }
*/
export function evadeImportExpressionTest(src) {
- const replaceFn = (_, p1) => `__import__${p1}`;
+ const replaceFn = (_, p1, p2) => `${p1}__import__${p2}`;
return src.replace(importPattern, replaceFn);
}
// /////////////////////////////////////////////////////////////////////////////
-const someDirectEvalPattern = new RegExp('\\beval(\\s*\\()', 'g');
+const someDirectEvalPattern = new RegExp('(^|[^.])\\beval(\\s*\\()', 'g');
/**
* Heuristically reject some text that seems to contain a direct eval

MODIFY
packages/ses/test/test-reject-direct-eval.js
packages/ses/test/test-reject-direct-eval.js
@@ -142,3 +142,13 @@ test('reject direct eval expressions with name', t => {
'newline with name',
);
});
+
+test('allow eval method invocation', t => {
+ t.plan(1);
+
+ const c = new Compartment({
+ evaler: { eval: t.pass },
+ });
+ const code = 'evaler.eval()';
+ c.evaluate(code);
+});

MODIFY
packages/ses/test/test-reject-import-expression.js
packages/ses/test/test-reject-import-expression.js
@@ -139,3 +139,13 @@ test('reject import expressions with error messages', t => {
'with name',
);
});
+
+test('allow import method invocation', t => {
+ t.plan(1);
+
+ const c = new Compartment({
+ importer: { import: t.pass },
+ });
+ const code = 'importer.import()';
+ c.evaluate(code);
+});

feat(ses): Allow import and eval methods (#669)