babel: [Bug]: Assumption privateFieldsAsProperties w/o transform-runtime plugin can lead to private field name clashes
đź’»
- Would you like to work on a fix?
How are you using Babel?
babel-loader (webpack)
Input code
When using the assumption privateFieldsAsProperties: true
in combination with not using the Babel transform-runtime
plugin (so that babel-helpers are inlined), it becomes possible to override #-private fields, which is incorrect semantics.
The issue can only be reproduced when using at least two different source files.
Bar.js:
export default class Bar {
#hidden = "Bar's hidden";
getHidden() {
return this.#hidden;
}
}
index.js:
import Bar from "./Bar";
class Foo extends Bar {
// it should not be possible to override a #private field of a superclass:
#hidden = "Foo's hidden";
}
console.log(new Foo().getHidden()); // actual: "Foo's hidden", expected: "Bar's hidden"
Since Babel’s own REPL only supports one input file, here is the full example including a .babelrc
in codesandbox:
codesandbox.io reproducer
Configuration file name
.babelrc
Configuration
{
"presets": [
"env"
],
"assumptions": {
"privateFieldsAsProperties": true
}
}
Current and expected behavior
When running the codesandbox example given above, in the output you see "Foo's hidden"
, so class Foo
managed to override the #-private field #hidden
of class Bar
, which must not be possible.
When you switch off privateFieldsAsProperties
and run the example again, the correct result "Foo's hidden"
is shown.
Also when you use the Babel plugin transform-runtime
, the behavior is correct.
Environment
See codesandbox example:
- Babel 7.2.0
- Node 16
Possible solution
The reason that both #-private fields share the same name __private_0_hidden
is that the helper function classPrivateFieldLooseKey
tries to define and initialize a global counter variable id
that is increased for each generated property name, to ensure unique private property names. However, when helper functions are inlined (no transform-runtime
plugin), each bundled file comes with a copy of this helper code, each defining its own var id = 0;
. Thus, in both bundled files, the private member counter starts with 0
, ending up in the same private field slot name in both the superclass and the subclass, which must not happen.
One approach to solve this could be to use a Symbol
instead of a generated string slot name, but I don’t know if Babel helper code is allowed to use language features that may not be available in older target environments. I tested this approach and it works perfectly (given that Symbol
is supported).
I guess helper code is not compiled, so using Symbol
would rule out IE, unless a Symbol
polyfill is applied. If this is a show-stopper, a new assumption privateFieldsAsSymbolProperties
could be introduced that fixes the issue, but requires a target that supports Symbol
(at least through a polyfill).
Additional context
No response
About this issue
- Original URL
- State: closed
- Created a year ago
- Comments: 36 (35 by maintainers)
Commits related to this issue
- fix: privateFieldsAsProperties global counter (#15389) When babel-helpers are inlined, the global variable `id` was defined and initialized to zero in every generated file. This could lead to name cl... — committed to fwienber/babel by fwienber a year ago
- fix: privateFieldsAsProperties global counter (#15389) When babel-helpers are inlined, the global variable `id` was defined and initialized to zero in every generated file. This could lead to name cl... — committed to fwienber/babel by fwienber a year ago
- fix: privateFieldsAsProperties global counter (#15389) When babel-helpers are inlined, classPrivateFieldLooseKey() defines a global variable `id` and initialized it to zero in every generated file. T... — committed to fwienber/babel by fwienber a year ago
- fix: privateFieldsAsProperties global counter (#15389) When babel-helpers are inlined, classPrivateFieldLooseKey() defines a global variable `id` and initialized it to zero in every generated file. T... — committed to fwienber/babel by fwienber a year ago
- fix: privateFieldsAsProperties global counter (#15389) When babel-helpers are inlined, classPrivateFieldLooseKey() defines a global variable `id` and initialized it to zero in every generated file. T... — committed to fwienber/babel by fwienber a year ago
- feature: new assumption privateFieldsAsSymbols, fixes #15389 When babel-helpers are inlined, classPrivateFieldLooseKey() defines a global variable `id` and initialized it to zero in every generated f... — committed to fwienber/babel by fwienber a year ago
Thanks @JLHwung, after your PR #15415 has been merged into main, rebasing on main did the trick and now all tests are green!
This is not related. It is from an upstream update. In e2e test the lock file is deleted to test against everything on the bleeding edge.
According to my understanding, the polyfill is injected on demand, and the assistant is not necessarily inserted directly into the code, it may also be a
require
orimport
, which will not import the dependent polyfill at all. (Even the inline code I’m not sure will be handled by the polyfill.)core-js
at least includes a polyfill forglobalThis
, moved to stable ES as of version 3.3.0. So I guess a babel helper could use it, unless another polyfill bundle is supported that does not include this polyfill.đź‘Ť, eval breaks under most CSPs.
The problem in this issue also happens without merging the files.
If you don’t use
@babel/runtime
, the private helper is duplicated in every file and thus it starts counting from0
every time.@fwienber Good idea! To save some bytes we can also make
__PRIVATE_FIELD_COUNTER__
a getter.If helper code runs in strict mode, no. The right-hand side
id
would not be defined, raising an error in strict mode.Not sure I got you right, but I think since the code is copied into every generated file, putting the
id
in a closure is not an option, there is no way around using some “global variable”, i.e. some property of the global JavaScript object. Since we cannot useSymbol
s, it has to have some (cryptic) name. What language features / ECMAScript level can we use in helper code? Arrow functions?globalThis
? Do they run in strict mode (I guess so)?