-
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
Conversation
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.
Some preliminary observations.
I'm having a hard time reviewing the tests because of the combined rename + modifications past the diff threshold. Would appreciate a minimal rename and separate modification to the tests
my biggest regret is that the diff is a bit difficult to review, but I think the result is easy to review and audit @mhofman the |
split the |
t.is( | ||
c.evaluate('globalThis[Symbol.unscopables] = { flag: true }; flag'), | ||
'safe', | ||
undefined, |
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.
What's preventing us from adding a noop setter on globalThis[Symbol.unscopables]
?
Would that be too much intervention?
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.
hmm we could do that
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.
I think it’s worth the loss in fidelity to prevent leaking the terminal scope proxy. What do you think, @erights?
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.
there is no known way to leak the scope terminator proxy. @naugtur's suggestion just prevents the "compartment global var not in scope" fidelity issue. from the pr top comment:
setting a correctly formatted Symbol.unscopables property on the evaluator globalObject will cause the therein configured globals to not be in scope. this is a shim fidelity issue and not a security issue as the scopeTerminator is not affected.
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.
What loss of fidelity are we talking about? The
noop setter on
globalThis[Symbol.unscopables]
?
?
And in exchange we would no longer leak a scope proxy? Yes, worth it!
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.
What loss of fidelity are we talking about?
setting a correctly formatted Symbol.unscopables
property on the evaluator globalObject will cause the globals configured by Symbol.unscopables
to not be in scope. this is a shim fidelity issue and not a security issue as the scopeTerminator
is not affected.
example:
globalThis.xyz = 123
xyz // => 123
globalThis[Symbol.unscopables] = { xyz: true }
xyz //=> undefined
And in exchange we would no longer leak a scope proxy? Yes, worth it!
there is no known way to leak a scope proxy in this PR's implementation
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.
I reread the conversation and understand that my previous response did not make sense.
I think the fidelity issue here cuts both ways, with no choice being strictly better fidelity than the other. The fidelity issue is also minor, as it is unlikely to be encountered by accident, and it is unlikely to be exploitable by an attacker. When faced with such a choice, the next question is: Which lack of fidelity fails safe?
I think giving the global object a Symbol.unscopables
non-configurable accessor without a setter, or whose setter always throws, means that any program that does not cause an error to be thrown will work the same under the shim and under a native implementation.
Unresolving.
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.
If we decide to install a Symbol.unscopables
on the globalThis
, it should be a configurable (to allow a program to change it through defineProperty
if really needed) inert accessor (to avoid assignment errors). I don't believe it should be a non-writable undefined
value, or a throwing setter, as programs with such assignments would end up throwing in strict mode.
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.
I have erred in my judgement, and have now come around to @erights's suggestion that we install a property with { configurable: false, enumerable: false, set() { assert.fail('Some meaningful message'); } }
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.
Appended commit fc8f84b
@mhofman makes sense - though im not sure how to accomplish this. can someone provide some suggestions on how to improve the legibility of the diff? my gitfoo is not amazing. and i dont think its possible to make a minimal modification to the |
@kumavis One way to satisfy @mhofman’s concern would be to either create or refer to tests that cover all the security behaviors of the collective scopes that pass both before and after this refactor, since the unit tests cut too closely to the particulars of the implementations before and after. If that requires new tests, we could land them in another PR before so we’re sure they passed before this change, and also highlight any deliberate behavior changes in this PR. |
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.
Intermediate review
Since the leak is no worse than what we have today, find by me too. |
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.
While I did not spot anything alarming, I did not give it a careful enough review to approve it on that basis alone. However, it's gotten good examination that I've followed and two approvals. I'm happy to see it merge on that basis, and the only way I can withdraw my "Request changes" is to approve.
So I approve. Good improvements! Onward!
packages/ses/src/make-evaluate.js
Outdated
|
||
/** | ||
* buildOptimizer() | ||
* Given an array of indentifier, the optimizer returns a `const` declaration |
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.
* Given an array of indentifier, the optimizer returns a `const` declaration | |
* Given an array of identifiers, the optimizer returns a `const` declaration |
packages/ses/src/make-evaluate.js
Outdated
function buildOptimizer(constants, name) { | ||
// No need to build an optimizer when there are no constants. | ||
if (constants.length === 0) return ''; | ||
// Use 'this' to avoid going through the scope proxy, which is unecessary |
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.
// Use 'this' to avoid going through the scope proxy, which is unecessary | |
// Use 'this' to avoid going through the scope proxy, which is unnecessary |
@@ -104,7 +106,6 @@ export const makeSafeEvaluator = ({ | |||
// variable resolution via the scopeHandler, and throw an error with | |||
// diagnostic info including the thrown error if any from evaluating the | |||
// source code. | |||
revokeScopeProxy(); |
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.
Wondering if we should neuter the evaluator in this case? Maybe something like replacing
allowNextEvalToBeUnsafe
with a throwing function?
I like that. Unless there's something surprising, I'd say that's worth doing.
e0c0d0e
to
79e77b9
Compare
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.
The "Revocable evalScope" commit LGTM
packages/ses/src/eval-scope.js
Outdated
// In makeSafeEvaluator, we ensure that we consume at least as many stack | ||
// frames when granting access to FERAL_EVAL than we consume when we are |
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.
// In makeSafeEvaluator, we ensure that we consume at least as many stack | |
// frames when granting access to FERAL_EVAL than we consume when we are | |
// In makeSafeEvaluator, we ensure that we consume at least as many stack | |
// frames when granting access to FERAL_EVAL as we consume when we are |
packages/ses/src/eval-scope.js
Outdated
// In this case, there is a plausible but very narrow window of opportunity | ||
// between defining `oneTimeEvalProperties` on the `evalScope` and when the | ||
// confined program reaches for the eval property on the `evalScope` through | ||
// a lexical reference where a program could induce a RangeError that would |
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.
// In this case, there is a plausible but very narrow window of opportunity | |
// between defining `oneTimeEvalProperties` on the `evalScope` and when the | |
// confined program reaches for the eval property on the `evalScope` through | |
// a lexical reference where a program could induce a RangeError that would | |
// In this case, there is a plausible but very narrow window of | |
// opportunity---between defining `oneTimeEvalProperties` on the `evalScope` and when the | |
// confined program reaches for the eval property on the `evalScope` through | |
// a lexical reference---where a program could induce a RangeError that would |
packages/ses/src/eval-scope.js
Outdated
// between defining `oneTimeEvalProperties` on the `evalScope` and when the | ||
// confined program reaches for the eval property on the `evalScope` through | ||
// a lexical reference where a program could induce a RangeError that would | ||
// prevent the program from reaching `delete evalScope.eval` either here or |
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.
// prevent the program from reaching `delete evalScope.eval` either here or | |
// prevent the program from reaching `delete evalScope.eval`, either here or |
Even with the extra punctuation, this is an amazing run-on sentence. Better to rephrase using more smaller sentences ;)
packages/ses/src/eval-scope.js
Outdated
// confined program reaches for the eval property on the `evalScope` through | ||
// a lexical reference where a program could induce a RangeError that would | ||
// prevent the program from reaching `delete evalScope.eval` either here or | ||
// in the fallback position in `makeSafeEvaluator` where we attempt to delete |
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.
// in the fallback position in `makeSafeEvaluator` where we attempt to delete | |
// in the fallback position in `makeSafeEvaluator`, where we attempt to delete |
How long is that sentence? ;)
packages/ses/src/eval-scope.js
Outdated
// it again in the finally clause. | ||
// By using `delete` and simple property assignment for | ||
// `evalScopeKit.revoked`, we avoid pushing to the stack while withdrawing | ||
// access to the `FERAL_EVAL`. |
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.
Worth noting the non-guaranteed implementation properties we assume in this reasoning that may well be wrong on particular platforms:
- That if stack A has more frames than stack B, where none of these frames are unusual, then stack B will not exceed the stack if stack A did not. May be false depending on the sizes of each frame.
- That function calls in the semantics of the language is an adequate predictor of stack frames. May be false when some of these function calls are inlined or otherwise optimized away --- as we know happens with JITs.
- Have we reexamined our frame-count reasoning in light of the possibility of implementation tail-calling? The ECMAScript std mandates it and JSC actually implements it.
The reason we're ok with this fragile reasoning is that it is only about a backstop that we'd need if all the other relevant mechanisms failed. Nothing will protect against any possible bug we overlooked. Rather, we're only trying for defense in depth, so that multiple improbably things would need to go wrong to create an exploitable vulnerability.
@@ -95,6 +97,7 @@ export const makeSafeEvaluator = ({ | |||
// variable resolution via the scopeHandler, and throw an error with | |||
// diagnostic info including the thrown error if any from evaluating the | |||
// source code. | |||
evalScopeKit.revoked = { err }; |
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.
Very nice communicating the error through the revocation channel!
Per #1329, we will no longer attempt to defend against poorly-vetted shims that generally break property descriptors.
*BREAKING CHANGE*: By breaking the scope proxy into four separate layers, SES will no longer leak scope proxies and consequently has removed the knownScopeProxies experimental feature.
Rewrite tests to verify functionality of evaluate instead asserting the string output of the factory.
*BREAKING CHANGE*: Adding a Symbol.unscopables property to globalThis in a compartment will now throw an error, as a preferable outcome to proceding under the false assumption that Symbol.unscopables do not affect global scope, as this is not a behavior of proper JavaScript that the shim can emulate.
79e77b9
to
3d022b1
Compare
multilayered scope stack, or "the four backflips of the apotheosis" 🏇 🤸 🧙
description
instead of
with(complicatedScopeProxy){ ... }
, we now have the following scope stack (outermost to innermost, lowest takes precedence):scopeTerminator
{security, fidelity} (always return undefined, ifsloppyGlobalsMode
is true, sets happen onglobalObject
)globalObject
{fidelity} (bare)globalLexicals
{fidelity} (bare)evalScope
{security} (obj withFERAL_EVAL
exposed aseval
only once)behavioural changes
this refactor introduces some changed behavior:
globalLexicals
object can be leaked if a special lexical is crafted (should not be craftable by evaluated code)Symbol.unscopables
property on the evaluator globalObject will cause the globals configured bySymbol.unscopables
to not be in scope. this is a shim fidelity issue and not a security issue as thescopeTerminator
is not affected.other notes
test-scope-handler
test to usemakeSafeEvaluator
and test the whole scope stack, but its not a great match and a lot had to be changedtest-scope-handler-pollution
because the test's prototype pollution broke themakeSafeEvaluator
internalsthings to revisit after this PR
has
proxy trap in safe evaluator’s terminal scope #1305moduleScope
andglobalLexicals
to limit impact of a leak Module shims lexicals leak internal live bindings object #912