Closed nuragic closed 5 years ago
I thought about exactly the same, but
they didn't want to tie it to one of the existing or future namespaces
e.g. System.global
and Object.global
. And Symbol.global
is practically the same. The fact that it's a symbol doesn't change much in this case. Symbols are helpful when used to avoid collisions between property names.
e.g. System.global and Object.global. And Symbol.global is practically the same
Keep in mind that Symbol
is quite different than Object
.
And about this statement from #32 by @ianstormtaylor Non-recommendations:
they didn't want to tie it to one of the existing or future namespaces, because it should be a global variable itself.
What are the specific arguments? I'd love to hear @ljharb feedback.
What are the specific arguments
I think to avoid things like System.global.System.global
, but I am not sure.
By the way, what even is System
¿? There's nothing in the spec about that.
Just for clarifying, I'm talking about Symbol
.
https://www.ecma-international.org/ecma-262/index.html#sec-ecmascript-language-types-symbol-type
It's a hypothetical namespace, suggested for containing global
. If I recall correctly it was once upon a time suggested also as a host to import
like this: System.import
when module syntax has been debated.
Thanks @nuragic for the suggestion! You’re totally right that symbols can avoid collisions - imo that’s one of their primary values.
However, one of the requirements is that this feature be deniable - meaning, that in a given scope, code can be denied access to it, or it can be virtualized across different “compartments” in the same realm. Attaching it to any shared value - like Object or Symbol or any constructor - means that symbols themselves couldn’t be shared across compartments without also sharing the global (or indirectly, a key on the global).
As such, it needs to be an identifier - as if it was created by var
in the top level of a Script.
@ljharb is it possible to explain this a bit simpler? Alternatively, can you explain please why and by whom
code can be denied access to it
I will be very grateful for the explanation. Thanks!
One use case is salesforce, which needs to be able to safely run untrusted JavaScript code in the webpage (i believe for custom plugins? Not 100% sure of the details) caja / SES is a library used for this purpose.
cc @erights, @caridy
As explained at https://medium.com/agoric/pola-would-have-prevented-the-event-stream-incident-45653ecbda99 https://news.ycombinator.com/item?id=18590116 The recent npm / event-stream security debacle is the perfect teaching moment for POLA (Principle of Least Authority), and for the need to support least authority for JavaScript libraries.
https://www.youtube.com/watch?v=9Snbss_tawI&list=PLKr-mvz8uv... is my presentation to the Node security team, explaining many of these issues prior to this particular incident.
At the November 2018 tc39 meeting, I presented on enhancements needed to JavaScript modules to provide good support for least authority libraries.
Many thanks for the feedback! I have a couple more questions... :)
@ljharb
... or it can be virtualized across different “compartments” in the same realm.
What do you mean by compartments? Also, realm as defined in the current spec? Or as defined in the proposal at stage 2?
Another question is: why is not possible to deny access to e.g. Symbol.global
? What if that would not be shared across realms?
Unless otherwise specified, well-known symbols values are shared by all realms
The Realms proposal shim at https://github.com/tc39/proposal-realms/tree/master/shim incorporated in SES https://github.com/Agoric/SES explained at https://www.youtube.com/watch?v=9Snbss_tawI&list=PLKr-mvz8uv...%C2%A0is
defines a taxonomy with two fundamental kinds of realms: "root realms" and "compartments". This unbundles two aspects of current Realms (in browsers and Node) into two separate concepts:
Each root realm has an independent set of primordials, where primordials are those objects, other than the global object, that is mandated by the JavaScript spec to exist before code starts running. An array expression evaluated in one root realms is not instanceof Array
for the Array
of another root realm.
A realm --- both root realms and compartment --- is
eval
and Function
) that evaluate code in that global scope.A compartment is a realm that is not a root realm. Rather, a compartment is created within a root realm, and uses the root realm's primordials as its primordials. When the root realm's primordials are frozen, as both Salesforce https://www.youtube.com/watch?v=3ME7oHHQbuM and Agoric are doing, then these compartments are featherweight protection domains.
@erights Is there a constraint which says that the "global object" cannot be available as a property of some other object in the global scope (e.g. FooBar.global
)?
Wow, thanks for the lesson Sir @erights 🙏❤
I think I'll need to carefully read / watch all the available material to better understand all the details and implications.
Oh, I think I see how compartments play into it. If intrinsics are going to be shared between compartments, then all of their properties will be shared as well. But we don't really want to share a property pointing to a "global object" among compartments.
More to the point, if the property value is shared, then it's not going to be the current "global object" that we want.
(Please correct me if that's wrong.)
Hi @zenparsing that's exactly right!
@erights Do compartments share the Realm
instrinsic? It seems like they can't, assuming that you want compartments to be able to create child realms.
Each root realm has its own class-like Realm
global.
Realm.makeRootRealm()
makes a new root realm, and makes an instance of that Realm
to represent the new realm. The new realm instance object is an object within the realm of the Realm
class that made it. However, this is the only way that a new root realm is connected to the realm that made it. Otherwise, root realms are conceptually independent of each other.
Realm.makeCompartment()
makes a new compartment within the root realm of that Realm
class. It has all the same intrinsics but a distinct global object, global scope, and evaluators.
Thus, there is a Realm
class per root realm and a Realm
instance per realm.
So given a rootRealm
and two compartments within that realm, compartmentA
and compartmentB
:
rootRealm.global.Realm === compartmentA.global.Realm;
compartmentA.global.Realm === compartmentB.global.Realm;
Correct?
Is the following expression true
or false
?
compartmentA.global.Realm.makeRootRealm ===
compartmentB.global.Realm.makeRootRealm;
All yes. This works because a root realm does not care which compartment made it. IOW it is made by a parent root realm, not a parent realm.
Unless configured otherwise. Alice could set up compartment Bob with a global Realm
customized for Bob's use.
Thanks. I had some additional questions about realms but I can add a thread to the realms repo.
My original line of thinking was that since the realms API presupposed some surface area for getting a global object it might make sense to place the API to get the current global object somewhere in that API as well.
@erights Out of curiousity, if code had some way of getting a Realm
instance representing the realm of the current execution context would that present any POLA issues? Would such a Realm
instance provide any capabilities that executing code does not already posses?
if code had some way of getting a Realm instance representing the realm of the current execution context would that present any POLA issues?
For a compartment, it would be fine. For a root realm, it would be a disaster, as the instance representing the root realm is an object of the parent realm. Code in the parent realm typically wants to be unreachable from the root realms it creates.
That makes sense. I was imagining that any mechanism that allows me to reflect upon my own realm would result in an object of my own realm.
I agree with concerns raised by the OP in this thread but don’t think Symbol
makes sense here.
It strikes me that Reflect
acts as a source of methods for manipulating objects which are available in other ways. Perhaps Reflect.global
could serve as a similar, immutable way of accessing the global scope?
Hi @liamnewmarch no, the same concern applies. There cannot be a reference from any primordial in general use back to the global. Doing so would break compartment isolation and virtualization.
We are fortunate that there is no such reference currently. This is a language invariant we must maintain.
What's "OP"?
@erights “original post” or “original poster”
@liamnewmarch in the past when I’ve explored adding things to reflect, I’ve been told that the sole purpose of Reflect is to provide API methods, 1:1, for Proxy traps - so nothing can go on there that isn’t a trap. That’s why Reflect.enumerate was removed when the trap was removed, eg.
Again, thank you really much for the additional feedback.
After going back and forth reading the spec, this proposal, realms proposal, this thread, etc. I finally understand the implications of just sharing a reference to the global object in any primordial constructor, e.g. Symbol.global
. It is as simple as @erights perfectly summarised:
Doing so would break compartment isolation and virtualization.
Period.
However, in my very last attempt to find an alternative solution, I was trying some stuff, and came up with this hack:
class getGlobal extends Function {
constructor() {
return super('return this');
}
}
console.log('this', new getGlobal().call());
the only reliable means to get the global object is
Function('return this')()
Well, the above seems to work inside ESM... am I dreaming or something?
Hi @nuragic , if you are dreaming, then the Function
constructor is our shared nightmare ;)
In all seriousness, that's why each compartment gets its own Function
constructor, its own eval
function, its own global scope, and its own global object. For everything else, the compartment shares primordials with its enclosing root realm, and therefore with other compartments within that same root realm.
There's one remaining issue that Realms solve with a necessary kludge. All of these Function
constructors within the same root realm share the same Function.prototype
. As a result, a function made by any of these Function constructors is still instanceof Function
as tested against any of these other Function constructors. No problem so far.
This shared prototype, however, has a .constructor
property that can only point at one thing. We point it at the Function
constructor of the root realm, which we uniquely disable by default. This one Function
constructor, when called, throws an error rather than returning a function.
Object-capability rules are not the reason. In SES for example, the root realm has its own global that is completely harmless---it points only at the root realm's primordials, all of whom are also harmless. It would violate no ocap safety property for the root realm's Function
constructor to evaluate code in the scope of that harmless global.
Rather, @mikesamuel has raised pointed out (link?) the difficulty in reviewing code if innocent code doing reasonable things can be confused into accidentally calling an evaluator. The case is something like
x[c1][c2](s)
c1
happens to be 'constructor'
, looking up, say, the Object
constructor.c2
happens to be 'constructor'
, looking up the Function
constructor.s
is code that does something that is only surprising.Even if an attacker can influence the contents of c1
, c2
, and s
, this code still does not let them cause any unauthorized effects for the above reasons. But it does make code harder to review if the attacker can introduce arbitrary behavior into a program that never explicitly mentions eval
or Function
.
In all seriousness, that's why each compartment gets its own Function constructor
Interesting, Function
doesn't have the same compartment constraint as other things mentioned in this thread...?
For your code
class getGlobal extends Function {
constructor() {
return super('return this');
}
}
If evaluated within a compartment, the Function
mentioned here would be the Function
constructor of that compartment, which evaluates code within the scope of that compartment's global. According to the Realm proposal, this would indeed obtain the compartments global, which is consistent with the way in which we treat the compartment as a protection domain.
Curiously, in both the Realms shim and in SES this code would still not obtain even that compartment's global, because the replacement evaluators installed by the shim and used by SES force all evaluated code to be evaluated in strict mode.
Because this Function
constructor cannot be found by naive indexed property dereferencing, @mikesamuel's attack still fails. Unlike the root realm, here @mikesamuel 's attack would matter because a compartment's global can carry authority, so hidden evaluation in that scope could be dangerous. From that pov, Function.prototype.constructor !== Function
within a compartment is a feature, with a necessary cost in compatibility. In all the Google and Salesforce experience with this incompatibility, I don't think it ever broke anything.
Interesting, Function doesn't have the same compartment constraint as other things mentioned in this thread...?
Which compartment constraint do you have in mind?
@nuragic fwiw the reason that works is because eval still works - I believe that in a CSP-enabled realm, that code would not work? (if it does, that seems like a bug in CSP :-p )
@ljharb I think you're right. I'll try to test it in a real chrome app, which has a strict enough default CSP policy. Anyways, would be getting the "global this" still a problem on there, given that they let you sandbox your app? https://developer.chrome.com/extensions/sandboxingEval
I’ve had many bugs filed on es5-shim and es6-shim by folks trying to use it in chrome apps (ie, in a csp context) before i moved from the robust Function approach to a more brittle “test all the possibilities” approach.
@ljharb
You were indeed right. Moreover, I guess that trying to hack CSP is not in the scope of this proposal, neither in my original post's intent. 😄
Given all the constraints plus your precious feedback I came up with this. I'd love to see any issue opened on there to discuss that particular alternative. Also, feel free to close this issue if you consider it appropriate. Thanks!
Could Realm.global
could be a getter on the realm constructor (compartment or otherwise) which simply (and somewhat magically ala eval) returns the global of the caller?
No. This is a form of magic --- implicit parameterization --- that we avoid at all costs. It is a form of dynamic scoping. It breaks reasoning about equivalence of program transformation. It causes confused deputies. This cure is much much worse than the alleged disease it addresses.
Note that indirect eval has no such magic. Direct eval is not magical about its caller, it is magical about its lexical context.
@zenparsing another example of that is the import()
expression, which is contextualized. We definitely want to stay away from that for the global object.
Sometimes I haz bad ideas. 💣
I'm afraid we have another case of a proposal that is restricted by a set of contraints that are not simultaneously resolvable. I'm assuming that one of those constraints is that the feature should be generally acceptable to users.
The feature should be; i don’t consider it a constraint that the name is acceptable to users, since most every name we’ve picked for anything has many users who think it’s a bad name.
Rather, @mikesamuel has raised pointed out (link?) the difficulty in reviewing code if innocent code doing reasonable things can be confused into accidentally calling an evaluator. The case is something like
I think the link you're talking about https://medium.com/@mikesamuel/puzzling-towards-security-a12b9427124
“A very simple piece of code” shows a subtle security problem. I use that to discuss the role code review plays in security. Later in the series I discuss ways to enable effective, targeted review even in highly dynamic, rapidly evolving systems.
The accompanying code demonstrates the implicit access to eval
problem.
To understand the following, note that SES does not support sloppy code.
@mikesamuel yes, that is the one I was thinking of. In SES, all the *Function*
constructors in the root-realm primordials throw an error rather than evaluate. The only remaining root-realm evaluator is the eval
function itself, which is accessible from the root-realm's global, and evaluates code in the root realm's global scope, with its top-level this
being the root-realm's global object. The eval
function and the root realm's global are not otherwise available from any of the root-realm's primordials.
In the root realm's global scope, Function.prototype.contrustor === Function
, which thereby does not lead to a working evaluator.
Compartments each have their own working eval
function and Function
constructor, which evaluates in the global scope of that compartment. Code evaluated by a compartment's evaluators can only reach that compartment's evaluators either by mentioning them as global variables eval
or Function
, or by mentioning a top level this
to obtain that compartment's global object. All of these can be statically detected. If rare enough, they are practically reviewable. The only computed property access for obtaining a working evaluator without mention it requires that the compartment's global object is mentioned first.
You can try subverting these defenses at https://rawgit.com/Agoric/SES/master/demo/ . If you find any flaws, please responsibly disclose them first as explained at https://github.com/Agoric/SES#bug-disclosure
Do you agree that this successfully addresses the threat model you raise?
This also helps explain why I don't much care for this proposal in any of its guises. The more normal it becomes to mention the global object and pass it around as first class data, the less reviewable code becomes regarding this thread model. Thus, we subtract value by making it more normal to mention the global object. However, I have given up trying to stop this proposal. I'm just trying to ensure it does as little damage as possible, and does no irreparable damage. Fortunately, this proposal as is does not violate any fundamental ocap principles.
However, I have given up trying to stop this proposal. I'm just trying to ensure it does as little damage as possible, and does no irreparable damage. Fortunately, this proposal as is does not violate any fundamental ocap principles.
Yes, that makes sense and of course I want to preserve ocap principles as well. Unfortunately, when you take the intersection of all acceptable solutions (including users' and educators' points of view) we get, literally, the empty set.
Again, though, that’s only if we conflate the semantics/behavior with the aesthetics/syntax/naming - “a global identifier” satisfies everyone, the only contention is around teachability/confusion/aesthetics of the chosen identifier.
This gets us into philosophy I suppose, but for me, syntax/naming and semantics are separate, but of equal importance. After all, users only ever experience the semantics through the syntax. And the syntax is how the concepts are first presented.
Aethetics are important! 🎨
Absolutely they are :-) it’s important to not pretend they’re the same as the rest of the proposal, as well.
when you take the intersection of all acceptable solutions (including users' and educators' points of view) we get, literally, the empty set.
To be clear, I prefer the empty set. However I realize this is a minority position, so I am willing to accept something mildly worse than the empty set.
Thanks to everyone for your feedback! It's very appreciated, and helps all of us that use JavaScript.
I've attempted to document the constraints here, including suggestions from this thread, and as discussed above, I'll close this issue. Please feel free to discuss further in this issue, or to file new ones as appropriate!
First, sorry for the reaction in #32, but as almost everyone, I'm very concerned about adding a global variable called
globalThis
. I believe it's a bad idea for the ergonomics and the future of the language. So this is my last (and I hope more productive) attempt aimed to help fixing this situation.It's a shame this good proposal took this direction now but unfortunately there's one huge problem: naming collision. Seems almost impossible to create a global variable that doesn't break the web and has a name which... well, doesn't break the web community.
The basic idea would be to add a new well-known Symbol
@@global
which references to[[globalThis]]
.That's it.
Would it alter the current, desired behavior?
I know, this is basically a different proposal, but honestly... if it'd work then I guess it will be worth.
Thanks for your work and for your time.