Repository navigation
Identifiers inside functions nested in class field initializers are neither renamed nor preserved (stringArray produces X is not a function) #1462
Description
Expected Behavior
Identifiers declared in a function nested inside a class field initializer, such as the parameter b in static staticField = register({ onChange: (b) => { ... } }), are renamed like identifiers in any other function, and the obfuscated code runs correctly. The minimal example below should print foobar.
Current Behavior
Those identifiers are neither renamed nor preserved, so a generated name can take the same name. In the example below, the string array calls wrapper is also named b. The string array scope calls wrapper j inserted into the nested function therefore receives the shadowing parameter instead of the calls wrapper, and the code throws TypeError: j is not a function:
class Foo
{
static [i(0xc0)] = register
({
'onChange': (b) =>
{
var j = b;
return b[j(0xb6)]+b[j(0xb5)];
}
});
}
// ...
function b(c,d){c=c-0xb4;var e=a();var f=e[c];return f;}The same function written inside a class method instead of a class field initializer has its parameter renamed correctly.
This is easy to hit with minified input: minifiers commonly name the first parameter e, and with mangled or mangled-shuffled identifier names the string array calls wrapper can get the same single-letter name. Our production build crashed this way on code shaped like static someProperty = register({ onChanged: e => { ... } }).
Steps to Reproduce
npm install javascript-obfuscator@5.9.0- Save the minimal working example below as
repro.js - Run
node repro.js
JavaScript Obfuscator Edition
- JavaScript Obfuscator Open Source
Your Environment
- Obfuscator version used: 5.9.0 (also reproduced with 5.7.0)
- Node version used: 24.18.0
Stack trace
TypeError: j is not a function
at Object.onChange (eval at <anonymous> (repro.js:28:13), <anonymous>:1:758)
at eval (eval at <anonymous> (repro.js:28:13), <anonymous>:1:804)
at Object.<anonymous> (repro.js:28:13)
at Module._compile (node:internal/modules/cjs/loader:1871:14)
at Object..js (node:internal/modules/cjs/loader:2002:10)
at Module.load (node:internal/modules/cjs/loader:1594:32)
at Module._load (node:internal/modules/cjs/loader:1396:12)
at wrapModuleLoad (node:internal/modules/cjs/loader:255:19)
at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5)
at node:internal/main/run_main_module:33:47
Minimal working example that will help to reproduce issue
const JavaScriptObfuscator = require('javascript-obfuscator');
const code = `
function register (options) {
return options;
}
class Foo {
static staticField = register({
onChange: (b) => {
return b['value'] + b['suffix'];
}
});
}
Foo.staticField.onChange({ value: 'foo', suffix: 'bar' });
`;
const obfuscatedCode = JavaScriptObfuscator.obfuscate(code, {
identifierNamesGenerator: 'mangled',
stringArray: true,
stringArrayThreshold: 1,
stringArrayWrappersCount: 1,
stringArrayWrappersChainedCalls: false
}).getObfuscatedCode();
console.log(obfuscatedCode);
console.log(eval(obfuscatedCode));Cause
ScopeIdentifiersTraverser.traverseScopeIdentifiersRecursive and traverseScopeThroughIdentifiersRecursive return early when currentScope.variableScope.block is not a node with a block lexical scope:
javascript-obfuscator/src/node/ScopeIdentifiersTraverser.ts
Lines 90 to 92 in 67005e2
| if (!variableLexicalScopeNode) { | |
| return; | |
| } |
eslint-scope 8 creates a class-field-initializer scope for every class field value. That scope is its own variable scope, and its block is the initializer expression itself. When the initializer is not a function, the early return also skips all child scopes, including functions nested inside the initializer. class-static-block scopes are not affected because StaticBlock is accepted by isNodeWithBlockLexicalScope.
I have a fix with tests ready and will open a PR referencing this issue.
Workaround
identifiersPrefix avoids the collision with the string array calls wrapper because generated global names get the prefix, but the identifiers inside the initializer are still not renamed, and generated local names can still collide with them.
Activity