From 99c635a6d2f87541e03ce4ef5dfa0026c050aba6 Mon Sep 17 00:00:00 2001 From: Timofey Kachalov Date: Tue, 27 Jan 2026 14:04:39 +0400 Subject: [PATCH] Fixed `controlFlowFlattening` breaking short-circuit evaluation with spread operator and conditional objects (#1373) --- CHANGELOG.md | 1 + .../LogicalExpressionControlFlowReplacer.ts | 31 +++++++ .../issues/fixtures/issue1372.js | 15 ++++ .../functional-tests/issues/issue1372.spec.ts | 90 +++++++++++++++++++ ...ConditionalCommentObfuscatingGuard.spec.ts | 6 +- 5 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 test/functional-tests/issues/fixtures/issue1372.js create mode 100644 test/functional-tests/issues/issue1372.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 013df569..c289cdda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Change Log v5.2.0 --- * Skip obfuscation of `process.env.*` +* Fixed `controlFlowFlattening` breaking short-circuit evaluation with spread operator and conditional objects. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1372 v5.1.0 --- diff --git a/src/node-transformers/control-flow-transformers/control-flow-replacers/LogicalExpressionControlFlowReplacer.ts b/src/node-transformers/control-flow-transformers/control-flow-replacers/LogicalExpressionControlFlowReplacer.ts index d77f1c60..d4f5d626 100644 --- a/src/node-transformers/control-flow-transformers/control-flow-replacers/LogicalExpressionControlFlowReplacer.ts +++ b/src/node-transformers/control-flow-transformers/control-flow-replacers/LogicalExpressionControlFlowReplacer.ts @@ -2,6 +2,7 @@ import { inject, injectable } from 'inversify'; import { ServiceIdentifiers } from '../../../container/ServiceIdentifiers'; import * as ESTree from 'estree'; +import * as estraverse from '@javascript-obfuscator/estraverse'; import { TControlFlowCustomNodeFactory } from '../../../types/container/custom-nodes/TControlFlowCustomNodeFactory'; import { TIdentifierNamesGeneratorFactory } from '../../../types/container/generators/TIdentifierNamesGeneratorFactory'; @@ -88,6 +89,10 @@ export class LogicalExpressionControlFlowReplacer extends ExpressionWithOperator leftExpression: ESTree.Expression, rightExpression: ESTree.Expression ): boolean { + if (this.expressionContainsProhibitedNodes(rightExpression)) { + return true; + } + return [leftExpression, rightExpression].some((expressionNode: ESTree.Node | ESTree.Expression): boolean => { let nodeForCheck: ESTree.Node | ESTree.Expression; @@ -105,4 +110,30 @@ export class LogicalExpressionControlFlowReplacer extends ExpressionWithOperator ); }); } + + /** + * @param {Expression} expression + * @returns {boolean} + */ + private expressionContainsProhibitedNodes(expression: ESTree.Expression): boolean { + let hasProhibitedNode = false; + + estraverse.traverse(expression, { + enter: (node: ESTree.Node): estraverse.VisitorOption | void => { + if (NodeGuards.isMemberExpressionNode(node) && node.computed) { + hasProhibitedNode = true; + + return estraverse.VisitorOption.Break; + } + + if (NodeGuards.isCallExpressionNode(node)) { + hasProhibitedNode = true; + + return estraverse.VisitorOption.Break; + } + } + }); + + return hasProhibitedNode; + } } diff --git a/test/functional-tests/issues/fixtures/issue1372.js b/test/functional-tests/issues/fixtures/issue1372.js new file mode 100644 index 00000000..cc1dfb95 --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue1372.js @@ -0,0 +1,15 @@ +function test(hasColor) +{ + const accent = hasColor ? "primary" : undefined; + const theme = { palette: { primary: { main: "ok" } }}; + + const obj = + { + bgcolor: "test", + ...(accent && { color: theme.palette[accent].main }) + } + + return obj; +} + +module.exports = { test }; diff --git a/test/functional-tests/issues/issue1372.spec.ts b/test/functional-tests/issues/issue1372.spec.ts new file mode 100644 index 00000000..9b5da5f3 --- /dev/null +++ b/test/functional-tests/issues/issue1372.spec.ts @@ -0,0 +1,90 @@ +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/1372 +// +describe('Issue #1372', () => { + describe('Spread operator with conditional object should work correctly', () => { + const samplesCount = 50; + + let obfuscatedCode: string; + + it('does not break on run with controlFlowFlattening enabled', () => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1372.js'); + + for (let i = 0; i < samplesCount; i++) { + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + controlFlowFlattening: true, + controlFlowFlatteningThreshold: 1, + stringArray: true, + stringArrayThreshold: 1 + }).getObfuscatedCode(); + + const result = eval(` + ${obfuscatedCode} + module.exports; + `); + + const resultTrue = result.test(true); + assert.deepEqual(resultTrue, { bgcolor: 'test', color: 'ok' }); + + const resultFalse = result.test(false); + assert.deepEqual(resultFalse, { bgcolor: 'test' }); + } + }); + + it('does not break on run with transformObjectKeys enabled', () => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1372.js'); + + for (let i = 0; i < samplesCount; i++) { + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + transformObjectKeys: true, + stringArray: true, + stringArrayThreshold: 1 + }).getObfuscatedCode(); + + const result = eval(` + ${obfuscatedCode} + module.exports; + `); + + const resultTrue = result.test(true); + assert.deepEqual(resultTrue, { bgcolor: 'test', color: 'ok' }); + + const resultFalse = result.test(false); + assert.deepEqual(resultFalse, { bgcolor: 'test' }); + } + }); + + it('does not break on run with both controlFlowFlattening and transformObjectKeys enabled', () => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1372.js'); + + for (let i = 0; i < samplesCount; i++) { + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + controlFlowFlattening: true, + controlFlowFlatteningThreshold: 1, + transformObjectKeys: true, + stringArray: true, + stringArrayThreshold: 1 + }).getObfuscatedCode(); + + const result = eval(` + ${obfuscatedCode} + module.exports; + `); + + const resultTrue = result.test(true); + assert.deepEqual(resultTrue, { bgcolor: 'test', color: 'ok' }); + + const resultFalse = result.test(false); + assert.deepEqual(resultFalse, { bgcolor: 'test' }); + } + }); + }); +}); diff --git a/test/functional-tests/node-transformers/preparing-transformers/obfuscating-guards/conditional-comment-obfuscating-guard/ConditionalCommentObfuscatingGuard.spec.ts b/test/functional-tests/node-transformers/preparing-transformers/obfuscating-guards/conditional-comment-obfuscating-guard/ConditionalCommentObfuscatingGuard.spec.ts index c8822dfb..bb4edbdb 100644 --- a/test/functional-tests/node-transformers/preparing-transformers/obfuscating-guards/conditional-comment-obfuscating-guard/ConditionalCommentObfuscatingGuard.spec.ts +++ b/test/functional-tests/node-transformers/preparing-transformers/obfuscating-guards/conditional-comment-obfuscating-guard/ConditionalCommentObfuscatingGuard.spec.ts @@ -134,11 +134,11 @@ describe('ConditionalCommentObfuscatingGuard', () => { const ignoredFunctionExpression1RegExp: RegExp = /var bar *= *function *\(a, *b, *c\) *{/; const ignoredFunctionExpression2RegExp: RegExp = /var baz *= *function *\(a, *b, *c\) *{/; - const obfuscatedFunctionCallRegExp: RegExp = /_0x([a-f0-9]){5,6}\( *\);/g; + const obfuscatedFunctionCallRegExp: RegExp = /_0x([a-f0-9]){5,6}\( *\)[,;]/g; const expectedObfuscatedFunctionCallsLength: number = 3; - const ignoredFunctionCall1RegExp: RegExp = /bar\( *\);/; - const ignoredFunctionCall2RegExp: RegExp = /baz\( *\);/; + const ignoredFunctionCall1RegExp: RegExp = /bar\( *\)[,;]/; + const ignoredFunctionCall2RegExp: RegExp = /baz\( *\)[,;]/; let obfuscatedCode: string, obfuscatedFunctionExpressionMatchesLength: number,