Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ses): Patch leak of globalLexicals thru moduleLexicals #1341

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions packages/ses/NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
User-visible changes in SES:

# Next release

- *SECURITY FIX*: Closes a leak for `globalLexicals`, such that a module
lexical scope cannot be used to reveal names in global lexical scope.

# v0.17.0 (2022-10-24)

- Previous versions of SES would leak the proxy used to isolate evaluated
Expand Down
16 changes: 9 additions & 7 deletions packages/ses/src/compartment-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
TypeError,
arrayPush,
create,
defineProperties,
getOwnPropertyDescriptors,
} from './commons.js';
import {
Expand All @@ -30,8 +29,11 @@ export const provideCompartmentEvaluator = (compartmentFields, options) => {
let { globalTransforms } = compartmentFields;
const { globalObject, globalLexicals } = compartmentFields;

let localObject = globalLexicals;
if (__moduleShimLexicals__ !== undefined) {
let moduleLexicals;
if (
__moduleShimLexicals__ !== undefined &&
__moduleShimLexicals__ !== null
Comment on lines +34 to +35
Copy link
Contributor

Choose a reason for hiding this comment

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

This is really one of those cases where I prefer to write __moduleShimLexicals__ != null

) {
// When using `evaluate` for ESM modules, as should only occur from the
// module-shim's module-instance.js, we do not reveal the SES-shim's
// module-to-program translation, as this is not standardizable behavior.
Expand All @@ -42,16 +44,16 @@ export const provideCompartmentEvaluator = (compartmentFields, options) => {
// and `import`, at the expense of being tightly coupled to SES-shim.
globalTransforms = undefined;

localObject = create(null, getOwnPropertyDescriptors(globalLexicals));
defineProperties(
localObject,
moduleLexicals = create(
null,
getOwnPropertyDescriptors(__moduleShimLexicals__),
);
}

({ safeEvaluate } = makeSafeEvaluator({
globalObject,
globalLexicals: localObject,
globalLexicals,
moduleLexicals,
globalTransforms,
sloppyGlobalsMode,
}));
Expand Down
31 changes: 22 additions & 9 deletions packages/ses/src/make-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,20 @@ function buildOptimizer(constants, name) {
*
* @param {object} context
* @param {object} context.evalScope
* @param {object} context.globalLexicals
* @param {object} context.globalObject
* @param {object} context.globalLexicals
* @param {object} context.moduleLexicals
Comment on lines -29 to +31
Copy link
Contributor

Choose a reason for hiding this comment

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

wondering if we should keep the order the same as in the nested scopes

* @param {object} context.scopeTerminator
*/
export const makeEvaluate = context => {
const { globalObjectConstants, globalLexicalConstants } = getScopeConstants(
const {
globalObjectConstants,
globalLexicalConstants,
moduleLexicalConstants,
} = getScopeConstants(
context.globalObject,
context.globalLexicals,
context.moduleLexicals,
);
const globalObjectOptimizer = buildOptimizer(
globalObjectConstants,
Expand All @@ -43,6 +49,10 @@ export const makeEvaluate = context => {
globalLexicalConstants,
'globalLexicals',
);
const moduleLexicalOptimizer = buildOptimizer(
moduleLexicalConstants,
'moduleLexicals',
);
Comment on lines +52 to +55
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this ever actually happen? I mean this is probably the safest way, and doesn't cost much.

Copy link
Member Author

Choose a reason for hiding this comment

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

Not in any correct arrangement. Any potential overlap would need to be censored by a transform.


// Create a function in sloppy mode, so that we can use 'with'. It returns
// a function in strict mode that evaluates the provided code using direct
Expand Down Expand Up @@ -93,13 +103,16 @@ export const makeEvaluate = context => {
with (this.scopeTerminator) {
with (this.globalObject) {
with (this.globalLexicals) {
with (this.evalScope) {
${globalObjectOptimizer}
${globalLexicalOptimizer}
return function() {
'use strict';
return eval(arguments[0]);
};
with (this.moduleLexicals) {
with (this.evalScope) {
${globalObjectOptimizer}
${globalLexicalOptimizer}
${moduleLexicalOptimizer}
return function() {
'use strict';
return eval(arguments[0]);
};
}
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions packages/ses/src/make-safe-evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ const { details: d } = assert;
* @param {Object} [options.globalLexicals]
* @param {Array<Transform>} [options.globalTransforms]
* @param {bool} [options.sloppyGlobalsMode]
* @param {Object} [options.moduleLexicals]
*/
export const makeSafeEvaluator = ({
globalObject,
globalLexicals = {},
moduleLexicals = {},
globalTransforms = [],
sloppyGlobalsMode = false,
} = {}) => {
Expand All @@ -36,6 +38,7 @@ export const makeSafeEvaluator = ({

const evaluateContext = freeze({
evalScope,
moduleLexicals,
globalLexicals,
globalObject,
scopeTerminator,
Expand Down
19 changes: 18 additions & 1 deletion packages/ses/src/scope-constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,17 +145,32 @@ function isImmutableDataProperty(obj, name) {
*
* @param {Object} globalObject
* @param {Object} globalLexicals
* @param {Object} moduleLexicals
*/
export const getScopeConstants = (globalObject, globalLexicals = {}) => {
export const getScopeConstants = (
globalObject,
globalLexicals = {},
moduleLexicals = {},
) => {
// getOwnPropertyNames() does ignore Symbols so we don't need to
// filter them out.
const globalObjectNames = getOwnPropertyNames(globalObject);
const globalLexicalNames = getOwnPropertyNames(globalLexicals);
const moduleLexicalNames = getOwnPropertyNames(moduleLexicals);

// Collect all valid & immutable identifiers from the endowments.
const moduleLexicalConstants = arrayFilter(
moduleLexicalNames,
name =>
isValidIdentifierName(name) &&
isImmutableDataProperty(moduleLexicals, name),
);

// Collect all valid & immutable identifiers from the endowments.
const globalLexicalConstants = arrayFilter(
globalLexicalNames,
name =>
!arrayIncludes(moduleLexicalNames, name) &&
isValidIdentifierName(name) &&
isImmutableDataProperty(globalLexicals, name),
);
Expand All @@ -167,6 +182,7 @@ export const getScopeConstants = (globalObject, globalLexicals = {}) => {
name =>
// Can't define a constant: it would prevent a
// lookup on the endowments.
!arrayIncludes(moduleLexicalNames, name) &&
!arrayIncludes(globalLexicalNames, name) &&
isValidIdentifierName(name) &&
isImmutableDataProperty(globalObject, name),
Expand All @@ -175,5 +191,6 @@ export const getScopeConstants = (globalObject, globalLexicals = {}) => {
return {
globalObjectConstants,
globalLexicalConstants,
moduleLexicalConstants,
};
};
2 changes: 1 addition & 1 deletion packages/ses/test/test-compartment.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ test('main use case', t => {
t.throws(() => user(-1), { instanceOf: TypeError });
});

test.failing('compartment does not leak globalLexicals through moduleLexicals', t => {
test('compartment does not leak globalLexicals through moduleLexicals', t => {
const compartment = new Compartment(null, null, {
globalLexicals: {
secret: 'secret',
Expand Down
24 changes: 21 additions & 3 deletions packages/ses/test/test-make-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,43 @@ const makeObservingProxy = target => {
};

test('makeEvaluate - optimizer', t => {
t.plan(5);
t.plan(6);

const globalObjectTarget = Object.create(null, {
foo: { value: true },
bar: { value: true },
baz: { value: true, writable: true },
});
const globalLexicalsTarget = Object.create(null, { foo: { value: false } });
const moduleLexicalsTarget = Object.create(null, { qux: { value: false } });

const [globalObject, globalObjectOps] = makeObservingProxy(
globalObjectTarget,
);
const [globalLexicals, globalLexicalsOps] = makeObservingProxy(
globalLexicalsTarget,
);
const [moduleLexicals, moduleLexicalsOps] = makeObservingProxy(
moduleLexicalsTarget,
);

const scopeTerminator = strictScopeTerminator;
const evalScopeKit = makeEvalScopeKit();
const { evalScope } = evalScopeKit;

const evaluate = makeEvaluate(
freeze({ scopeTerminator, globalObject, globalLexicals, evalScope }),
freeze({
scopeTerminator,
globalObject,
globalLexicals,
moduleLexicals,
evalScope,
}),
);

t.deepEqual(globalObjectOps, [['get', 'bar']]);
t.deepEqual(globalLexicalsOps, [['get', 'foo']]);
t.deepEqual(moduleLexicalsOps, [['get', 'qux']]);

globalObjectOps.length = 0;
globalLexicalsOps.length = 0;
Expand All @@ -63,13 +74,20 @@ test('makeEvaluate - strict-mode', t => {

const globalObject = Object.create(null);
const globalLexicals = Object.create(null);
const moduleLexicals = Object.create(null);

const scopeTerminator = strictScopeTerminator;
const evalScopeKit = makeEvalScopeKit();
const { evalScope } = evalScopeKit;

const evaluate = makeEvaluate(
freeze({ scopeTerminator, globalObject, globalLexicals, evalScope }),
freeze({
scopeTerminator,
globalObject,
globalLexicals,
moduleLexicals,
evalScope,
}),
);

evalScopeKit.allowNextEvalToBeUnsafe();
Expand Down
Loading