-
Notifications
You must be signed in to change notification settings - Fork 106
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
Scoping down the use of PrivateName #133
Comments
First the problem: Key to the "hard private" protection model that TC39 has adopted is that access to private fields is lexically limited to code that statically occurs as part of a class definition that is within the scope of the private field definition. Key hard private invariant: It is possible to expose external access to private fields but only if the author of a class definition has included source code in the definition that exposes that accesses. For example: let exposedAccessor;
class WithExposedField {
#exposed;
constructor() {
if (!exposedAccessor) {
//a static initialization block would be a better way to do this
exposedAccessor = {
get: obj => obj.#exposed,
set: (obj, value) => obj.#exposed = value
}
}
}
}
let instance = new WithExposedField;
exposedAccessor.set(instance, "set from outside of class def"); Private fields, in the absence of the decorator proposal, provides no way for an attacker to gain access to a private field other than by changing source code before it is loaded. Private Names objects, as defined in the current decorators proposal, breaks this invariant in three ways:
It's fairly easy to remediate the 1st and 3rd item. A simple step is to scope the useful lifetime of each PrivateName object to the duration of the ClassDefinitionEvaluation that exposes the PrivateName object via an element or class descriptor. This could work by ClassDefinitionEvaluation maintaining a List of PrivateName objects it has exposed and invalidating each of those instances (by nulling out its [[PrivateNameDate]] internal slot before exiting (either normally or abruptly) from ClassDefinitionEvaluation. The 3rd item could be address by making ClassDefinitionEvaluation refuse to create any private fields using Private Name objects that are not in its List of exposed Private names. The 2nd problem is more fundamental. Private Name objects expose a read/write capability to enable decorators to instantiate decorator generated functions that read/write private fields. Such functions are typically installed as the values of method properties or get/set functions of accessor properties. Using access to a lexically captured PrivateName object with read/write capability is a way to allow a decorator author to code such privileged functions. Because, they are evaluated long after ClassDefinitionEvaluation completes limiting the effective lifetime of PrivateName objects or revoking their read/write capability breaks that use case. The Private Name object read/write capability is a fundamental violation of the Key Hard Private Invariant. It enables a third party (the author of a decorator) to access a private field that the author of a class definition has not explicit exposed. I don't believe that there is a way to support all of the decorator use cases that require Private Name objects with read/write capability without violating the Key Hard Private Invariant. To me that suggest we should abandon the goal of supporting those use cases. In TC39 we routinely make trade-offs between expressibility and other goals such as security or usability. There are many attractive features that we have left out of the language because of such trade-offs. This is such a situation. All of the decorator use cases that require Private Name read/write capability are possible using Symbol keyed "soft private" fields. The only reason to have hard private fields in the language is to provide an ultimate level of secure integrity to objects that really require it. In all other situations soft private is more expressive. It seems to me that if decorators expose persistent Private Name objects with read/write capabilities we will have eliminated the primary reasons for the existence of private fields. cc/ @erights @zenparsing @ waldemarhorwat |
Do you think the invariant is still a problem if the private field itself is decorated, as well as a class containing them - or only when the class is decorated? |
@ljharb The field decorator might be something that sounds useful and not a threat to the privacy of a private field. I'm primarily thinking about Trojan horse decorators. It would be easy enough for somebody to publish on NPM an attractive sounding decorator module that actually does useful things but under the covers leaks access to certain private fields. Also, there are various ways that attackers might gain access to mutable bindings that are used to access decorators and substitute a Trojan. |
@allenwb: I think this misses a critical use case if we're not able to install new private names onto classes. For example, a common theme in frameworks it to cause a re-render when a property is changed: class Comp extends Framework {
// When count changes, re-render the component
count = 1;
render() {
return html`<span>${this.count}</span>`;
}
} The full-proof way to do this is to make a getter-setter pair, but it's cumbersome. You also have to think about where does the getter-setter store it's state? Usually it's a class Comp extends Framework {
#count = 1;
get count() {
return this.#count;
}
set count(v) {
this.#count = v;
Framework.render(this.render());
}
render() {
return html`<span>${this.count}</span>`;
}
} It's a lot of boilerplate for a simple re-render. We had imagined an class Comp extends Framework {
@Framework.tracked
count = 1;
render() {
return html`<span>${this.count}</span>`;
}
} But, if If I can recategorize you're three points, they come down to two different issues:
From my code examples above, I don't believe we can solve 2 without forbidding useful code patterns. But we might be able to fix 1: There's another issue (#24) that essentially boils down to "keys are read/write privileged by default", which allows the kind of Trojan horse decorators you described. This is because the descriptor pass to the decorator call contains a fully privileged function dec(descriptor) {
// descriptor looks like:
// {
// key: privateNameInstance,
// initializer: () => 1
// }
const {key} = descriptor;
// assert(key instanceof PrivateName);
// assert(typeof privateName.get === 'function');
// assert(typeof privateName.set === 'function');
}
class Ex {
@dec
#count = 1;
} My current thinking is that the descriptor key for private fields should not be read/write privileged, it should just be able to install the internal slot onto class instances (let's call it a function dec(descriptor, privateNameReifier) {
const { key } = descriptor;
// assert(key instanceof PrivateNameKey);
// assert(key.get === undefined);
// assert(key.set === undefined);
const privateName = privateNameReifier(key);
// assert(privateName instanceof PrivateName);
// assert(typeof privateName.get === 'function');
// assert(typeof privateName.set === 'function');
}
class Ex {
@dec
#count = 1;
} So common operations on the descriptor pair might leak the |
I agree with your analysis that decorating private class elements requires trusting decorators. This proposal takes "hard private" with respect to decorators to mean that decorators are part of the class. That is, if you decorate a private class element or a class with private elements, you are deciding to trust the decorator with access to those elements.
I like your phrasing of how it's our trade-off to make. An alternative would be to not expose private class elements to decorators. This is a legitimate alternative that we discussed at the July 2018 TC39 meeting. In my view, a critical factor is that decorators on private class elements make privacy more usable in practice, by giving the flexibility to meet critical use cases. In the end, adding the ability to decorate private class elements means that the ecosystem will be better able to adopt private fields. I expect that, over time, limited, trusted and well-maintained decorator libraries will emerge which maintain good properties, the way we have limited, trusted libraries for membranes. At the July 2018 meeting, my understanding of the committee's response was widespread support for decorating private class elements, even understanding that this means giving the decorator these abilities. Most of the discussion was focused on how to make sure the private key does not accidentally leak to other code outside of the decorator; work is ongoing to make PrivateName a defensible class to achieve that goal. Sounds like, for Stage 3, we should reaffirm that the committee wants to take this trade-off. |
@jridgewell But I don't see how a design that is limited in that way could support your use cases. For example, a decorator that generator an accessor property for a private name would have to be able to return an element descriptor that contained get and set functions that access the private names. They would look something like: const { key } = descriptor;
const privateName = privateNameReifier(key);
...
return {
...
descriptor: {
get() { return privateName.get(this); },
set(value) {
privateName.set(this, value);
},
...
} The read/write capability of privateName couldn't be limited to the dynamic extent of a ClassDefinitionEvauation because the get/set accessor need to run on instantiated instances of the class. I haven't been able to think of a scheme that would let you write such accessor functions that could not also be used to leak the privateName read/write capability in an arbitrary manner.
I believe, read/write capability is the actual issue, create and install may be doable. ...or a Symbol based soft private field. And you're correct that isn't hard encapsulated. But neither are the current private fields in the presences of unverified decorators of the sort allowed by the current proposal. You need to either audit all third party decorators that you intend to use or simply never use a third party decorator on a class where you need hard encapsulation. I can imagine a linter rule: decorator used in a class that defines private fields. Soft encapsulation using Symbol keyed properties is a fine alternative if you don't actually need extreme hard encapsulation.
If you are talking about the presentation where both @waldemarhorwat and I raised our concerns then I don't think you can characterize that as TC39 making an informed decision. I wasn't clear to me that the majority of the attendees understood the issues or trade-offs involved. At the very least it seems like you need to have a serious security review of this feature that includes developing threat models for things like Trojan horse decorators. Then make sure TC39 actually understands the results of that review and has consensus on accepting any threats it identifies |
Maybe I misunderstood--i though @waldemarhorwat was advocating for a defensible class design. I've attempted to take that feedback into account in #134 . Could you tell me more about what you'd like to see in a security review? I tried to raise these issues in the July 2018 TC39 meeting; it would be helpful to understand what was missing from that presentation. |
@jridgewell What do you think of @rbuckton's proposal in this area? It seems largely similar to yours, mostly differing in what is the default. Do you see your version as providing more protection? |
In my example, the reifier function is only lives for the let key;
let reifier;
let privateName;
function dec(descriptor, privateNameReifier) {
// Let's extract these to demonstrate lifetimes
key = descriptor.key;
reifier = privateNameReifier;
privateName = privateNameReifier(key);
return {
descriptor: {
get() { return privateName.get(this); },
set(value) {
privateName.set(this, value);
},
};
}
class Ex {
@dec
count = 1;
}
// And after `ClassDefinitionEvauation`:
assert(privateName.get(new Ex) === 1);
assert.throws(() => {
reifier(key) // throws TypeError
});
// Key could be installed onto another class,
// but its a lot of code to demonstrate
That's my intention with the reifier, to make any accesses to
There's a 4 comment back-and-forth that explains my position:
I think privileged-by-default makes things much less secure than unprivileged. Although I do really like the idea of a |
@jridgewell I don't understand the proposal in a couple ways:
All together, this seems like a big change and erognomics regression to prevent certain kinds of accidental leaks. I wonder, what if we mitigated the issue by publishing a correct version of the |
I have a feeling that if we try to use a language-based mechanism to restrict the use of PrivateNames, beyond using defensible classes and solving for certain use cases like @rbuckton's proposal, it will be a never-ending battle as more cases of passing around PrivateNames are discussed. I'm not totally convinced by the idea here that adding a field isn't a meaningful operation in terms of privacy. It may be less significant, but private names may also be used for "secure" brand checks, so it's not nothing to be able to falsify these. By including decorators on private names, we are deciding to give developers the power and control over whether they expose these names to others, one way or another. As @allenwb says above, it is important to think carefully about whether we want to provide this power. |
Correct. From the
The think the reifier would be valid for any syntatic private fields in the class body. We could decide instead to be scoped down to only the private field if you're just decorating that. But definitely reifiers are not the same function for every class decoration, eg. I can't extract a private name using a reifier from a previous class: let reifier;
function extractReifier(_, privateNameReifier) {
reifier = privateNameReifier;
}
@extractReifier class Extractor { #x = 1 };
// reifier should not have access to later class' privates:
function trojan(descriptor) {
// notice using old reifier
assert.throws(() => {
const extracted = reifier(descriptor.key);
});
}
@trojan class Ex { #x = 2 }
Potentially. I am a fan of its explicit "I don't trust this decorator" source text, but I feel like it's a really weird solution to force every end user to use in their code. It's kind of like, if the decorator isn't explicitly dealing with a private field, it should always include this redact. My reifier makes that the default (and also makes the redact function simpler -- at the end of the comment), so nothing has to worry about securing private fields unless it explicitly deals with them.
Likely. From my original 3-level security comment, I'm only going to fight over L1 leaks (I think it's absolute critical that we secure at least that). It seems @allenwb and @waldemarhorwat have a little apprehension on allowing these L2 leaks. |
Perhaps, rather than an @decoratorThatCannotSeeX
@trusted: decoratorThatCanSeeX
class C {
#x
} |
This is the part that I don't understand. I think a @rbuckton This is an interesting idea. We could say, if you decorate a private method or private field, you're clearly syntactically giving it trust, but if you decorate a class, it could go either way. So omitting the If we want to go down this route, I'd say that we could leave To make it concrete, rather than a |
There's been some concern about the trust involved in decorating private class elements. Decorating a private field or method is specified to give access to the private name which is declared. Although this notion of privacy may be defensible in theory, we take a couple additional precautions to ensure that it's private in practice: - #134: PrivateName is a defensive class, which is deeply frozen and does not permit monkey patching. - #133: Class decorators get "restricted" PrivateName instances, which cannot be used to read or write private class elements. Previously, the idea was that class decorators would be trusted, enabling class decorators to be used to deliberately grant access to private fields and methods to things outside of the class. For example, a @testable decorator could expose all private class elements to a testing framework. The new idea is, class decorators are less trusted, and can only see that private class elements are present, insert or delete them, but not read or write them. This increases the level of privacy in practice (e.g., if a decorator is used for wrapping purposes and not expected to be able to trusted reading private fields), but removes certain use cases. This patch enables class decorators to add and remove private elements, which may be considered to have integrity implications with respect to usages of private class elements as a "brand". The trust level of class decorators is intermediate: they are trusted with these branding rights, but not with the ability to actually read and write everything. In a follow-on proposal, we could permit class decorators to get un-restricted private names through a `@trusted: decorator` syntax; we could also decide to "relax" the restriction on all class decorators. The logic here could be used to implement "restricted" private fields as proposed in #24 (comment) with the following implementation: ```js function restricted(privateName) { let restrictedName; function classDecorator({[{key}]}) { restrictedName = key; } function elementDecorator(descriptor) { return {...descriptor, key: privateName}; } @classDecorator class X { @elementDecorator x; } return restrictedName; } ```
@allenwb @waldemarhorwat @jridgewell It would be great to hear from you if you feel that #136 addresses the problem, or if more changes are needed. |
This patch makes it so that class decorators do not see private class elements. With this patch, only element decorators have the ability to do metaprogramming on private class elements. For field/method decorators, granting access to the private name seems syntactically clear and implied by the construct. There are also several critical use cases, including: - Limited-capability accessors (@reader) - Observation pattern (@Tracked) - Friends/testable (@testable) For access from class decorators to private, different people have had different intuitions: Some expect access, while others say it violates privacy. A workaround here, under this patch, is to use an element decorator for each private element, rather than a class decorator, to accomplish the same thing. This may be ugly, but it works for all the use cases I'm aware of. As a possible follow-on proposal, a specifically "trusted" type of decorator could be created, though the details are unclear. See #133
This patch makes it so that class decorators do not see private class elements. With this patch, only element decorators have the ability to do metaprogramming on private class elements. For field/method decorators, granting access to the private name seems syntactically clear and implied by the construct. There are also several critical use cases, including: - Limited-capability accessors (@reader) - Observation pattern (@Tracked) - Friends/testable (@testable) For access from class decorators to private, different people have had different intuitions: Some expect access, while others say it violates privacy. A workaround here, under this patch, is to use an element decorator for each private element, rather than a class decorator, to accomplish the same thing. This may be ugly, but it works for all the use cases I'm aware of. As a possible follow-on proposal, a specifically "trusted" type of decorator could be created, though the details are unclear. See #133
We ended up merging #151 to address this issue. However, issues later appeared, and I'd like to reconsider this decision. Apologies for the wall of text that follows--this is a letter I sent to @erights' SES mailing list to explain the rationale. I would be interested in your thoughts here as well. Hi SES folks, tl;dr: Should we go back and let class decorators access private fields In the SES review of the Decorators proposal, the big actionable Thinking it through a bit more, we realized that it's not really It turns out, though, that there are very important use cases for both
Note that other programming languages with strong encapsulation It seems to me that, after a thorough investigation, we have determined I think we can make granting this additional power acceptable through Thinking a bit broader, I'd like to encourage the mental model of Thoughts? |
This reverts PR #151. Rationale in #133 (comment)
) This reverts PR #151. Rationale in #133 (comment)
Initially, the decorators proposal had class descriptors which contained an elements array with public and private elements. Later, concerns were raised about exposing this private information unexpectedly (#133), and this access was removed (#151). However, this patch caused a problem: private field initializers were accidentally sorted after public field initializers, an observable and counterintuitive change (#183). Halfway solutions (#184) did not really work out; we added back the full element support in #133 (comment) . Although we believe that it makes sense for decorators to be able to see into class bodies, including private, and there are many important use cases (for both public and private, #192), this support doesn't have strict consensus in committee, so this patch serves as a conservative option in case including elements is unacceptable. This is not the preferred alternative, but it's a well-articulated backup plan. I'm writing this out in advance in the interest of time, to ensure that we have a path to Stage 3 and that this does not block things.
At this point, I see two feasible alternatives:
I prefer that we include |
See related issue #191 . |
Private name values have been removed from the current draft, so I'm closing this issue. |
In the July 2018 TC39 meeting, @allenwb mentioned somehow restricting the use of PrivateName down somewhat, to avoid some kinds of leakage, but I didn't quite understand what he was suggesting. Allen, would you be able to comment here with more details?
The text was updated successfully, but these errors were encountered: