From d6d51dbfd86096128f2a1924e32dd8ce93b52828 Mon Sep 17 00:00:00 2001 From: sanex3339 Date: Tue, 27 Jan 2026 19:35:52 +0400 Subject: [PATCH 1/3] Fixed `transformObjectKeys` incorrectly hoisting object literal outside of loop --- CHANGELOG.md | 4 + package.json | 2 +- .../ObjectExpressionKeysTransformer.ts | 36 ++++++++- src/node/NodeGuards.ts | 21 +++++ .../issues/fixtures/issue1300-do-while.js | 9 +++ .../issues/fixtures/issue1300-for-in.js | 8 ++ .../issues/fixtures/issue1300-for-of.js | 7 ++ .../issues/fixtures/issue1300-while.js | 8 ++ .../issues/fixtures/issue1300.js | 8 ++ .../functional-tests/issues/issue1300.spec.ts | 79 +++++++++++++++++++ 10 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 test/functional-tests/issues/fixtures/issue1300-do-while.js create mode 100644 test/functional-tests/issues/fixtures/issue1300-for-in.js create mode 100644 test/functional-tests/issues/fixtures/issue1300-for-of.js create mode 100644 test/functional-tests/issues/fixtures/issue1300-while.js create mode 100644 test/functional-tests/issues/fixtures/issue1300.js create mode 100644 test/functional-tests/issues/issue1300.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fbbaf6f5..f64fba88e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ Change Log +v5.2.1 +--- +* Fixed `transformObjectKeys` incorrectly hoisting object literal outside of loop when loop body is a single statement without braces, causing all iterations to share the same object reference. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1300 + v5.2.0 --- * Skip obfuscation of `process.env.*` diff --git a/package.json b/package.json index 7bb02fa54..4baf28c3f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-obfuscator", - "version": "5.2.0", + "version": "5.2.1", "description": "JavaScript obfuscator", "keywords": [ "obfuscator", diff --git a/src/node-transformers/converting-transformers/ObjectExpressionKeysTransformer.ts b/src/node-transformers/converting-transformers/ObjectExpressionKeysTransformer.ts index e0addf21f..d35f21472 100644 --- a/src/node-transformers/converting-transformers/ObjectExpressionKeysTransformer.ts +++ b/src/node-transformers/converting-transformers/ObjectExpressionKeysTransformer.ts @@ -127,7 +127,8 @@ export class ObjectExpressionKeysTransformer extends AbstractNodeTransformer { ObjectExpressionKeysTransformer.isProhibitedSequenceExpression( objectExpressionNode, objectExpressionHostStatement - ) + ) || + ObjectExpressionKeysTransformer.isProhibitedLoopBody(objectExpressionNode) ) { return true; } @@ -140,6 +141,39 @@ export class ObjectExpressionKeysTransformer extends AbstractNodeTransformer { return hasReferencedIdentifier || hasCallExpression; } + /** + * @param {ObjectExpression} objectExpressionNode + * @returns {boolean} + */ + private static isProhibitedLoopBody(objectExpressionNode: ESTree.ObjectExpression): boolean { + let currentNode: ESTree.Node | undefined = objectExpressionNode; + + while (currentNode) { + const parentNode: ESTree.Node | undefined = currentNode.parentNode; + + if (!parentNode) { + break; + } + + const isNonBlockLoopBody: boolean = + NodeGuards.isLoopStatementNode(parentNode) && + parentNode.body === currentNode && + !NodeGuards.isBlockStatementNode(currentNode); + + if (isNonBlockLoopBody) { + return true; + } + + if (NodeGuards.isFunctionNode(parentNode)) { + break; + } + + currentNode = parentNode; + } + + return false; + } + /** * @param {ObjectExpression} objectExpressionNode * @param {Node} objectExpressionNodeParentNode diff --git a/src/node/NodeGuards.ts b/src/node/NodeGuards.ts index b34962a39..da88ffecd 100644 --- a/src/node/NodeGuards.ts +++ b/src/node/NodeGuards.ts @@ -330,6 +330,27 @@ export class NodeGuards { return node.type === NodeType.LogicalExpression; } + /** + * @param {Node} node + * @returns {boolean} + */ + public static isLoopStatementNode( + node: ESTree.Node + ): node is + | ESTree.ForStatement + | ESTree.ForInStatement + | ESTree.ForOfStatement + | ESTree.WhileStatement + | ESTree.DoWhileStatement { + return ( + NodeGuards.isForStatementNode(node) || + NodeGuards.isForInStatementNode(node) || + NodeGuards.isForOfStatementNode(node) || + NodeGuards.isWhileStatementNode(node) || + NodeGuards.isDoWhileStatementNode(node) + ); + } + /** * @param {Node} node * @returns {boolean} diff --git a/test/functional-tests/issues/fixtures/issue1300-do-while.js b/test/functional-tests/issues/fixtures/issue1300-do-while.js new file mode 100644 index 000000000..ddb2f0380 --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue1300-do-while.js @@ -0,0 +1,9 @@ +(function() { + let arr = []; + let i = 0; + do + arr.push({value: 0}); + while (++i < 3); + arr[0].value = 1; + return arr[0] === arr[1]; +})(); diff --git a/test/functional-tests/issues/fixtures/issue1300-for-in.js b/test/functional-tests/issues/fixtures/issue1300-for-in.js new file mode 100644 index 000000000..162f0e7c1 --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue1300-for-in.js @@ -0,0 +1,8 @@ +(function() { + let arr = []; + let obj = {a: 1, b: 2, c: 3}; + for (let key in obj) + arr.push({value: 0}); + arr[0].value = 1; + return arr[0] === arr[1]; +})(); diff --git a/test/functional-tests/issues/fixtures/issue1300-for-of.js b/test/functional-tests/issues/fixtures/issue1300-for-of.js new file mode 100644 index 000000000..01c3d6d67 --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue1300-for-of.js @@ -0,0 +1,7 @@ +(function() { + let arr = []; + for (let x of [1, 2, 3]) + arr.push({value: 0}); + arr[0].value = 1; + return arr[0] === arr[1]; +})(); diff --git a/test/functional-tests/issues/fixtures/issue1300-while.js b/test/functional-tests/issues/fixtures/issue1300-while.js new file mode 100644 index 000000000..e64e93267 --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue1300-while.js @@ -0,0 +1,8 @@ +(function() { + let arr = []; + let i = 0; + while (i++ < 3) + arr.push({value: 0}); + arr[0].value = 1; + return arr[0] === arr[1]; +})(); diff --git a/test/functional-tests/issues/fixtures/issue1300.js b/test/functional-tests/issues/fixtures/issue1300.js new file mode 100644 index 000000000..7d9fdc1c9 --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue1300.js @@ -0,0 +1,8 @@ +// Object inside for loop should create new object each iteration +(function() { + let arr = []; + for (let i = 0; i < 3; i++) + arr.push({value: 0}); + arr[0].value = 1; + return arr[0] === arr[1]; // should be false +})(); diff --git a/test/functional-tests/issues/issue1300.spec.ts b/test/functional-tests/issues/issue1300.spec.ts new file mode 100644 index 000000000..612be091a --- /dev/null +++ b/test/functional-tests/issues/issue1300.spec.ts @@ -0,0 +1,79 @@ +import { assert } from 'chai'; +import { NO_ADDITIONAL_NODES_PRESET } from '../../../src/options/presets/NoCustomNodes'; +import { readFileAsString } from '../../helpers/readFileAsString'; +import { JavaScriptObfuscator } from '../../../src/JavaScriptObfuscatorFacade'; + +// +// https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1300 +// +describe('Issue #1300', () => { + describe('Object inside loop should create new object each iteration', () => { + const samplesCount = 50; + + it('does not break object creation semantics with transformObjectKeys', () => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1300.js'); + + for (let i = 0; i < samplesCount; i++) { + const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + transformObjectKeys: true, + seed: i + }).getObfuscatedCode(); + + const originalResult = eval(code); + const obfuscatedResult = eval(obfuscatedCode); + + assert.equal(originalResult, false, 'Original code should return false'); + assert.equal(obfuscatedResult, false, `Obfuscated code should return false (seed: ${i})`); + } + }); + + it('does not break with for-in loop', () => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1300-for-in.js'); + + const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + transformObjectKeys: true + }).getObfuscatedCode(); + + assert.equal(eval(code), false); + assert.equal(eval(obfuscatedCode), false); + }); + + it('does not break with for-of loop', () => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1300-for-of.js'); + + const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + transformObjectKeys: true + }).getObfuscatedCode(); + + assert.equal(eval(code), false); + assert.equal(eval(obfuscatedCode), false); + }); + + it('does not break with while loop', () => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1300-while.js'); + + const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + transformObjectKeys: true + }).getObfuscatedCode(); + + assert.equal(eval(code), false); + assert.equal(eval(obfuscatedCode), false); + }); + + it('does not break with do-while loop', () => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1300-do-while.js'); + + const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + transformObjectKeys: true + }).getObfuscatedCode(); + + assert.equal(eval(code), false); + assert.equal(eval(obfuscatedCode), false); + }); + }); +}); From 92a6e7bf002d511093b75ebc6cc98cee4bb76fc3 Mon Sep 17 00:00:00 2001 From: sanex3339 Date: Tue, 27 Jan 2026 21:19:27 +0400 Subject: [PATCH 2/3] Fix tests --- .../ObjectExpressionKeysTransformer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node-transformers/converting-transformers/ObjectExpressionKeysTransformer.ts b/src/node-transformers/converting-transformers/ObjectExpressionKeysTransformer.ts index d35f21472..05bc0af6c 100644 --- a/src/node-transformers/converting-transformers/ObjectExpressionKeysTransformer.ts +++ b/src/node-transformers/converting-transformers/ObjectExpressionKeysTransformer.ts @@ -151,7 +151,7 @@ export class ObjectExpressionKeysTransformer extends AbstractNodeTransformer { while (currentNode) { const parentNode: ESTree.Node | undefined = currentNode.parentNode; - if (!parentNode) { + if (!parentNode || parentNode === currentNode) { break; } @@ -164,7 +164,7 @@ export class ObjectExpressionKeysTransformer extends AbstractNodeTransformer { return true; } - if (NodeGuards.isFunctionNode(parentNode)) { + if (NodeGuards.isFunctionNode(parentNode) || NodeGuards.isProgramNode(parentNode)) { break; } From d4ca395127c1cf64991b1c075bdf8479cabe2cbf Mon Sep 17 00:00:00 2001 From: sanex3339 Date: Tue, 27 Jan 2026 21:53:40 +0400 Subject: [PATCH 3/3] Update tests --- .../ObjectExpressionKeysTransformer.spec.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/functional-tests/node-transformers/converting-transformers/object-expression-keys-transformer/ObjectExpressionKeysTransformer.spec.ts b/test/functional-tests/node-transformers/converting-transformers/object-expression-keys-transformer/ObjectExpressionKeysTransformer.spec.ts index d261014a1..41b4e9eb9 100644 --- a/test/functional-tests/node-transformers/converting-transformers/object-expression-keys-transformer/ObjectExpressionKeysTransformer.spec.ts +++ b/test/functional-tests/node-transformers/converting-transformers/object-expression-keys-transformer/ObjectExpressionKeysTransformer.spec.ts @@ -1092,13 +1092,13 @@ describe('ObjectExpressionKeysTransformer', () => { }); describe('Variant #2: without block statement', () => { + // Object should NOT be transformed when inside loop without block statement + // to prevent all iterations sharing the same object reference (issue #1300) const match: string = `` + `var ${variableMatch};` + - `var ${variableMatch} *= *{};` + - `${variableMatch}\\['bar'] *= *'bar';` + `for *\\(var ${variableMatch} *= *0x0; *${variableMatch} *< *0xa; *${variableMatch}\\+\\+\\) *` + - `${variableMatch} *= *${variableMatch};` + + `${variableMatch} *= *\\{'bar': *'bar'\\};` + ``; const regExp: RegExp = new RegExp(match); @@ -1151,13 +1151,13 @@ describe('ObjectExpressionKeysTransformer', () => { }); describe('Variant #2: without block statement', () => { + // Object should NOT be transformed when inside loop without block statement + // to prevent all iterations sharing the same object reference (issue #1300) const match: string = `` + `var ${variableMatch} *= *{};` + - `var ${variableMatch} *= *{};` + - `${variableMatch}\\['bar'] *= *'bar';` + `for *\\(var ${variableMatch} in *${variableMatch}\\) *` + - `${variableMatch} *= *${variableMatch};` + + `${variableMatch} *= *\\{'bar': *'bar'\\};` + ``; const regExp: RegExp = new RegExp(match); @@ -1210,13 +1210,13 @@ describe('ObjectExpressionKeysTransformer', () => { }); describe('Variant #2: without block statement', () => { + // Object should NOT be transformed when inside loop without block statement + // to prevent all iterations sharing the same object reference (issue #1300) const match: string = `` + `var ${variableMatch} *= *\\[];` + - `var ${variableMatch} *= *{};` + - `${variableMatch}\\['bar'] *= *'bar';` + `for *\\(var ${variableMatch} of *${variableMatch}\\) *` + - `${variableMatch} *= *${variableMatch};` + + `${variableMatch} *= *\\{'bar': *'bar'\\};` + ``; const regExp: RegExp = new RegExp(match); @@ -1268,13 +1268,13 @@ describe('ObjectExpressionKeysTransformer', () => { }); describe('Variant #2: without block statement', () => { + // Object should NOT be transformed when inside loop without block statement + // to prevent all iterations sharing the same object reference (issue #1300) const match: string = `` + `var ${variableMatch};` + - `var ${variableMatch} *= *{};` + - `${variableMatch}\\['bar'] *= *'bar';` + `while *\\(!!\\[]\\)` + - `${variableMatch} *= *${variableMatch};` + + `${variableMatch} *= *\\{'bar': *'bar'\\};` + ``; const regExp: RegExp = new RegExp(match);