-
Notifications
You must be signed in to change notification settings - Fork 74
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
refactor(ses)!: Divide safe evaluator scope stack into four layers #1293
Merged
+840
−870
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
d66db7a
fix(ses): Typo in compartmentEvaluate
kriskowal a4ee1ea
fix(ses): Typo in scope-constants
kriskowal eb50902
test(ses): Remove property prototype pollution test
kriskowal 37c4b4a
refactor(ses)!: Divide scope proxy into four layers
kumavis ec21beb
test(ses): Remove obsolete known scope proxies tests
kumavis 8eff012
test(ses): Add scope terminator tests
kumavis a3df403
test(ses): Test evaluator evalScope
kumavis 2b202f7
test(ses): Adjust expectations of fidelity for global unscopables
kumavis dd6ea0b
test(ses): Include more with blocks in expected shape of magic lines
kumavis 53958d9
test(ses): Adjust expectations about scope behaviors
kumavis 3d9da89
test(ses): Remove obsolete scope handler tests
kumavis f70d046
refactor(ses): Colocate logic of exposing FERAL_EVAL inside eval-scop…
naugtur 83873f6
refactor(ses): Divide scope optimizer
kriskowal 32b42ea
refactor(ses): introduce makeEvaluate
mhofman eb4a2fd
refactor(ses): remove makeEvaluateFactory remnants
mhofman e8f5889
chore(ses): Update comments of makeEvaluate to reflect reality
mhofman dcb8f5d
fix(ses)!: Prevent surprising global unscopables behavior
kriskowal 0187d1e
feat(ses): Revocable evalScope
kriskowal 3d022b1
fix(ses): Protect necessary eval admission before it has been admitted
kriskowal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,6 @@ import { | |
ReferenceError, | ||
TypeError, | ||
WeakMap, | ||
WeakSet, | ||
arrayFilter, | ||
arrayJoin, | ||
assign, | ||
|
@@ -18,9 +17,9 @@ import { | |
promiseThen, | ||
weakmapGet, | ||
weakmapSet, | ||
weaksetHas, | ||
} from './commons.js'; | ||
import { | ||
setGlobalObjectSymbolUnscopables, | ||
setGlobalObjectConstantProperties, | ||
setGlobalObjectMutableProperties, | ||
setGlobalObjectEvaluators, | ||
|
@@ -119,12 +118,6 @@ export const CompartmentPrototype = { | |
return '[object Compartment]'; | ||
}, | ||
|
||
/* eslint-disable-next-line no-underscore-dangle */ | ||
__isKnownScopeProxy__(value) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to celebrate this change in
|
||
const { knownScopeProxies } = weakmapGet(privateFields, this); | ||
return weaksetHas(knownScopeProxies, value); | ||
}, | ||
|
||
module(specifier) { | ||
if (typeof specifier !== 'string') { | ||
throw new TypeError('first argument of module() must be a string'); | ||
|
@@ -277,20 +270,20 @@ export const makeCompartmentConstructor = ( | |
|
||
const globalObject = {}; | ||
|
||
setGlobalObjectSymbolUnscopables(globalObject); | ||
|
||
// We must initialize all constant properties first because | ||
// `makeSafeEvaluator` may use them to create optimized bindings | ||
// in the evaluator. | ||
// TODO: consider merging into a single initialization if internal | ||
// evaluator is no longer eagerly created | ||
setGlobalObjectConstantProperties(globalObject); | ||
|
||
const knownScopeProxies = new WeakSet(); | ||
const { safeEvaluate } = makeSafeEvaluator({ | ||
globalObject, | ||
globalLexicals, | ||
globalTransforms, | ||
sloppyGlobalsMode: false, | ||
knownScopeProxies, | ||
}); | ||
|
||
setGlobalObjectMutableProperties(globalObject, { | ||
|
@@ -313,7 +306,6 @@ export const makeCompartmentConstructor = ( | |
name: `${name}`, | ||
globalTransforms, | ||
globalObject, | ||
knownScopeProxies, | ||
globalLexicals, | ||
safeEvaluate, | ||
resolveHook, | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { FERAL_EVAL, create, defineProperties, freeze } from './commons.js'; | ||
|
||
import { assert } from './error/assert.js'; | ||
|
||
const { details: d } = assert; | ||
|
||
// We attempt to frustrate stack bumping attacks on the safe evaluator | ||
// (`make-safe-evaluator.js`). | ||
// A stack bumping attack forces an API call to throw a stack overflow | ||
// `RangeError` at an inopportune time. | ||
// The attacker arranges for the stack to be sufficiently deep that the API | ||
// consumes exactly enough stack frames to throw an exception. | ||
// | ||
// For the safe evaluator, an exception thrown between adding and then deleting | ||
// `eval` on `evalScope` could leak the real `eval` to an attacker's lexical | ||
// scope. | ||
// This would be sufficiently disastrous that we guard against it twice. | ||
// First, we delete `eval` from `evalScope` immediately before rendering it to | ||
// the guest program's lexical scope. | ||
// | ||
// If the attacker manages to arrange for `eval` to throw an exception after we | ||
// call `allowNextEvalToBeUnsafe` but before the guest program accesses `eval`, | ||
// it would be able to access `eval` once more in its own code. | ||
// Although they could do no harm with a direct `eval`, they would be able to | ||
// escape to the true global scope with an indirect `eval`. | ||
// | ||
// prepareStack(depth, () => { | ||
// (eval)(''); | ||
// }); | ||
// const unsafeEval = (eval); | ||
// const safeEval = (eval); | ||
// const realGlobal = unsafeEval('globalThis'); | ||
// | ||
// To protect against that case, we also delete `eval` from the `evalScope` in | ||
// a `finally` block surrounding the call to the safe evaluator. | ||
// The only way to reach this case is if `eval` remains on `evalScope` due to | ||
// an attack, so we assume that attack would have have invalided our isolation | ||
// and revoke all future access to the evaluator. | ||
// | ||
// To defeat a stack bumping attack, we must use fewer stack frames to recover | ||
// in that `finally` block than we used in the `try` block. | ||
// We have no reliable guarantees about how many stack frames a block of | ||
// JavaScript will consume. | ||
// Function inlining, tail-call optimization, variations in the size of a stack | ||
// frame, and block scopes may affect the depth of the stack. | ||
// The only number of acceptable stack frames to use in the finally block is | ||
// zero. | ||
// We only use property assignment and deletion in the safe evaluator's | ||
// `finally` block. | ||
// We use `delete evalScope.eval` to withhold the evaluator. | ||
// We assign an envelope object over `evalScopeKit.revoked` to revoke the | ||
// evaluator. | ||
// | ||
// This is why we supply a meaningfully named function for | ||
// `allowNextEvalToBeUnsafe` but do not provide a corresponding | ||
// `revokeAccessToUnsafeEval` or even simply `revoke`. | ||
// These recovery routines are expressed inline in the safe evaluator. | ||
|
||
export const makeEvalScopeKit = () => { | ||
const evalScope = create(null); | ||
const oneTimeEvalProperties = freeze({ | ||
eval: { | ||
get() { | ||
delete evalScope.eval; | ||
kumavis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return FERAL_EVAL; | ||
}, | ||
enumerable: false, | ||
configurable: true, | ||
}, | ||
}); | ||
|
||
const evalScopeKit = { | ||
evalScope, | ||
allowNextEvalToBeUnsafe() { | ||
if (evalScopeKit.revoked !== null) { | ||
// eslint-disable-next-line @endo/no-polymorphic-call | ||
assert.fail( | ||
d`a handler did not reset allowNextEvalToBeUnsafe ${this.revoked.err}`, | ||
); | ||
} | ||
// Allow next reference to eval produce the unsafe FERAL_EVAL. | ||
// We avoid defineProperty because it consumes an extra stack frame taming | ||
// its return value. | ||
defineProperties(evalScope, oneTimeEvalProperties); | ||
kriskowal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}, | ||
revoked: null, | ||
}; | ||
|
||
return evalScopeKit; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh. I don't remember noticing this section before.
No change suggested.