tc39 / proposal-global

ECMAScript Proposal, specs, and reference implementation for `global`
http://tc39.github.io/proposal-global/
MIT License
349 stars 18 forks source link

Symbol.global #33

Closed nuragic closed 5 years ago

nuragic commented 5 years ago

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.

anurbol commented 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.

nuragic commented 5 years ago

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.

anurbol commented 5 years ago

What are the specific arguments

I think to avoid things like System.global.System.global, but I am not sure.

nuragic commented 5 years ago

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

anurbol commented 5 years ago

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.

ljharb commented 5 years ago

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.

anurbol commented 5 years ago

@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!

ljharb commented 5 years ago

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

erights commented 5 years ago

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.

nuragic commented 5 years ago

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

erights commented 5 years ago

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

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.

zenparsing commented 5 years ago

@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)?

nuragic commented 5 years ago

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.

zenparsing commented 5 years ago

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.)

erights commented 5 years ago

Hi @zenparsing that's exactly right!

zenparsing commented 5 years ago

@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.

erights commented 5 years ago

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.

zenparsing commented 5 years ago

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;
erights commented 5 years ago

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.

erights commented 5 years ago

Unless configured otherwise. Alice could set up compartment Bob with a global Realm customized for Bob's use.

zenparsing commented 5 years ago

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?

erights commented 5 years ago

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.

zenparsing commented 5 years ago

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.

liamnewmarch commented 5 years ago

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?

erights commented 5 years ago

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.

erights commented 5 years ago

What's "OP"?

j-f1 commented 5 years ago

@erights “original post” or “original poster”

ljharb commented 5 years ago

@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.

nuragic commented 5 years ago

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?

erights commented 5 years ago

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)

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.

zenparsing commented 5 years ago

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...?

erights commented 5 years ago

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.

erights commented 5 years ago

Interesting, Function doesn't have the same compartment constraint as other things mentioned in this thread...?

Which compartment constraint do you have in mind?

ljharb commented 5 years ago

@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 )

nuragic commented 5 years ago

@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

ljharb commented 5 years ago

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.

nuragic commented 5 years ago

@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!

https://github.com/nuragic/proposal-global-reference

zenparsing commented 5 years ago

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?

erights commented 5 years ago

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.

caridy commented 5 years ago

@zenparsing another example of that is the import() expression, which is contextualized. We definitely want to stay away from that for the global object.

zenparsing commented 5 years ago

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.

ljharb commented 5 years ago

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.

mikesamuel commented 5 years ago

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.

erights commented 5 years ago

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.

zenparsing commented 5 years ago

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.

ljharb commented 5 years ago

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.

zenparsing commented 5 years ago

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! 🎨

ljharb commented 5 years ago

Absolutely they are :-) it’s important to not pretend they’re the same as the rest of the proposal, as well.

erights commented 5 years ago

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.

ljharb commented 5 years ago

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!