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

Most upvoted comments

Thanks @JLHwung, after your PR #15415 has been merged into main, rebasing on main did the trick and now all tests are green!

babel-old-version - babel-plugin-transform-unicode-regex - unicode-regex - negated-set fails with a diff

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 or import, 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 for globalThis, 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.

Personally, I think this is a bit dangerous, compared to the current problem.

đź‘Ť, eval breaks under most CSPs.

Can you explain why this project directly merges the output of multiple files without using IIFE or bundler?

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 from 0 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.

what about establishing a counter that is global even in case of inlined helper functions?

Then we might be able to do it directly based on id. Perhaps we can use closures or something to avoid such interference?

Also I recommend using something like iife to avoid global effects, as far as I know more than one transformation creates global variables.

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 use Symbols, 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)?