From 5e95732bc8521f02b8bb0dfcc856be63c1d50cde Mon Sep 17 00:00:00 2001 From: Timofey Kachalov Date: Thu, 9 Jul 2026 11:57:58 +0400 Subject: [PATCH 1/3] Fixed `controlFlowFlattening` intermittently dropping arguments of a spread call (e.g. `foo(...args)`) when it reused a control flow wrapper of a same-arity plain call (#1424) --- CHANGELOG.md | 4 +++ package.json | 2 +- .../CallExpressionControlFlowReplacer.ts | 25 ++++++++++++----- .../CallExpressionControlFlowReplacer.spec.ts | 27 +++++++++++++++++++ .../issue-1423-spread-and-plain-calls.js | 26 ++++++++++++++++++ 5 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 test/functional-tests/node-transformers/control-flow-transformers/control-flow-replacers/call-expression-control-flow-replacer/fixtures/issue-1423-spread-and-plain-calls.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fa607161..c51561e8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ Change Log +v5.4.5 +--- +* Fixed `controlFlowFlattening` intermittently dropping arguments of a spread call (e.g. `foo(...args)`) when it reused a control flow wrapper of a same-arity plain call. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1423 + v5.4.4 --- * Optimized scope identifiers transformer performance diff --git a/package.json b/package.json index 1caf7a7f4..2f3e5035d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-obfuscator", - "version": "5.4.4", + "version": "5.4.5", "description": "JavaScript obfuscator", "keywords": [ "obfuscator", diff --git a/src/node-transformers/control-flow-transformers/control-flow-replacers/CallExpressionControlFlowReplacer.ts b/src/node-transformers/control-flow-transformers/control-flow-replacers/CallExpressionControlFlowReplacer.ts index c02b59c17..e0895f51b 100644 --- a/src/node-transformers/control-flow-transformers/control-flow-replacers/CallExpressionControlFlowReplacer.ts +++ b/src/node-transformers/control-flow-transformers/control-flow-replacers/CallExpressionControlFlowReplacer.ts @@ -64,14 +64,27 @@ export class CallExpressionControlFlowReplacer extends AbstractControlFlowReplac const isChainExpressionParent = NodeGuards.isChainExpressionNode(parentNode); - // Bucket reuse-eligible wrappers by both arg count AND optional-ness, so an - // optional `foo?.(arg)` call never reuses the wrapper of a non-optional `bar(arg)` - // that happens to share `arguments.length` — which would drop the `?.` short-circuit - // and crash on undefined callees (issue #1408). - const replacerId: string = `${callExpressionNode.arguments.length}-${isChainExpressionParent ? 'optional' : 'standard'}`; + const expressionArguments: (ESTree.Expression | ESTree.SpreadElement)[] = callExpressionNode.arguments; + + // Bucket reuse-eligible wrappers by the exact argument *shape* (which positions are + // spread vs. plain) AND optional-ness — not just `arguments.length`. + // + // A spread call like `foo(...args)` has the same `arguments.length` as a plain + // `bar(arg)`, but the generated wrapper differs: `(callee, ...p1) => callee(...p1)` + // vs `(callee, p1) => callee(p1)`. Reusing a plain wrapper for a spread call collapses + // the spread to its first element and silently drops the remaining arguments (issue #1423). + // + // Encoding the per-argument shape also preserves the optional-ness bucketing that keeps + // an optional `foo?.(arg)` call from reusing a non-optional `bar(arg)` wrapper, which + // would drop the `?.` short-circuit and crash on undefined callees (issue #1408). + const argumentsShape: string = expressionArguments + .map((argument: ESTree.Expression | ESTree.SpreadElement): string => + NodeGuards.isSpreadElementNode(argument) ? 's' : 'p' + ) + .join(''); + const replacerId: string = `${argumentsShape}-${isChainExpressionParent ? 'optional' : 'standard'}`; const callExpressionFunctionCustomNode: ICustomNode> = this.controlFlowCustomNodeFactory(ControlFlowCustomNode.CallExpressionFunctionNode); - const expressionArguments: (ESTree.Expression | ESTree.SpreadElement)[] = callExpressionNode.arguments; callExpressionFunctionCustomNode.initialize(expressionArguments, isChainExpressionParent); diff --git a/test/functional-tests/node-transformers/control-flow-transformers/control-flow-replacers/call-expression-control-flow-replacer/CallExpressionControlFlowReplacer.spec.ts b/test/functional-tests/node-transformers/control-flow-transformers/control-flow-replacers/call-expression-control-flow-replacer/CallExpressionControlFlowReplacer.spec.ts index 0259d32d0..74b208362 100644 --- a/test/functional-tests/node-transformers/control-flow-transformers/control-flow-replacers/call-expression-control-flow-replacer/CallExpressionControlFlowReplacer.spec.ts +++ b/test/functional-tests/node-transformers/control-flow-transformers/control-flow-replacers/call-expression-control-flow-replacer/CallExpressionControlFlowReplacer.spec.ts @@ -276,5 +276,32 @@ describe('CallExpressionControlFlowReplacer', function () { } }); }); + + describe('Variant #9 - spread call must not reuse a plain wrapper of the same arity (issue #1423)', () => { + const samplesCount: number = 200; + + it('should forward every spread-expanded argument on every obfuscation when a spread call and a plain call share the same arity', () => { + const code: string = readFileAsString( + __dirname + '/fixtures/issue-1423-spread-and-plain-calls.js' + ); + + for (let i = 0; i < samplesCount; i++) { + const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + controlFlowFlattening: true, + controlFlowFlatteningThreshold: 1, + identifierNamesGenerator: 'mangled' + }).getObfuscatedCode(); + + const result: unknown = eval(obfuscatedCode); + + assert.strictEqual( + result, + 'ok', + `iteration ${i}: spread call reused a plain wrapper and dropped arguments (obfuscated code returned wrong value)` + ); + } + }); + }); }); }); diff --git a/test/functional-tests/node-transformers/control-flow-transformers/control-flow-replacers/call-expression-control-flow-replacer/fixtures/issue-1423-spread-and-plain-calls.js b/test/functional-tests/node-transformers/control-flow-transformers/control-flow-replacers/call-expression-control-flow-replacer/fixtures/issue-1423-spread-and-plain-calls.js new file mode 100644 index 000000000..ec2b11adf --- /dev/null +++ b/test/functional-tests/node-transformers/control-flow-transformers/control-flow-replacers/call-expression-control-flow-replacer/fixtures/issue-1423-spread-and-plain-calls.js @@ -0,0 +1,26 @@ +(function () { + function target (a, b, c) { + return '' + a + b + c; + } + + function id (x) { + return x; + } + + function forward () { + var rest = [1, 2, 3]; + + // A spread-forward call `target(...rest)` has `arguments.length === 1`, + // the same as the plain `id(x)` calls below. It must not reuse a plain + // single-argument control flow wrapper, otherwise every spread-expanded + // argument after the first is silently dropped (issue #1423). + id(1); + id(2); + id(3); + id(4); + + return target(...rest); + } + + return forward() === '123' ? 'ok' : 'broken'; +})(); From f3d0a64307fb95b7343566a523cb408dc3698a57 Mon Sep 17 00:00:00 2001 From: Timofey Kachalov Date: Thu, 9 Jul 2026 13:39:25 +0400 Subject: [PATCH 2/3] Fixed `selfDefending` making obfuscated code run several times slower on Bun/JavaScriptCore (#1425) --- CHANGELOG.md | 1 + .../templates/SelfDefendingTemplate.ts | 6 +- .../templates/SelfDefendingTemplate.spec.ts | 39 +++++++++++++ test/index.spec.ts | 1 + .../SelfDefendingRuntimePerformance.spec.ts | 55 +++++++++++++++++++ 5 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 test/performance-tests/SelfDefendingRuntimePerformance.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c51561e8b..322ca03e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Change Log v5.4.5 --- * Fixed `controlFlowFlattening` intermittently dropping arguments of a spread call (e.g. `foo(...args)`) when it reused a control flow wrapper of a same-arity plain call. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1423 +* Fixed `selfDefending` making obfuscated code run several times slower on Bun/JavaScriptCore. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1421 v5.4.4 --- diff --git a/src/custom-code-helpers/self-defending/templates/SelfDefendingTemplate.ts b/src/custom-code-helpers/self-defending/templates/SelfDefendingTemplate.ts index e488d8fb0..1fd2e4498 100644 --- a/src/custom-code-helpers/self-defending/templates/SelfDefendingTemplate.ts +++ b/src/custom-code-helpers/self-defending/templates/SelfDefendingTemplate.ts @@ -6,6 +6,10 @@ export function SelfDefendingTemplate (): string { return ` const {selfDefendingFunctionName} = {callControllerFunctionName}(this, function () { + if ({selfDefendingFunctionName}.bind().toString().indexOf('\\n') !== -1) { + return; + } + return {selfDefendingFunctionName} .toString() .search('(((.+)+)+)+$') @@ -13,7 +17,7 @@ export function SelfDefendingTemplate (): string { .constructor({selfDefendingFunctionName}) .search('(((.+)+)+)+$'); }); - + {selfDefendingFunctionName}(); `; } diff --git a/test/functional-tests/custom-code-helpers/self-defending/templates/SelfDefendingTemplate.spec.ts b/test/functional-tests/custom-code-helpers/self-defending/templates/SelfDefendingTemplate.spec.ts index f9633af01..370e6a206 100644 --- a/test/functional-tests/custom-code-helpers/self-defending/templates/SelfDefendingTemplate.spec.ts +++ b/test/functional-tests/custom-code-helpers/self-defending/templates/SelfDefendingTemplate.spec.ts @@ -240,4 +240,43 @@ describe('SelfDefendingTemplate', function () { }); }); }); + + describe('Variant #6: engines that re-serialize `toString` skip the beautifier trap (issue #1421)', () => { + const reserializingEngineSimulation: string = + '(function () {' + + 'var nativeBind = Function.prototype.bind;' + + 'Function.prototype.bind = function () {' + + 'var boundFunction = nativeBind.apply(this, arguments);' + + 'boundFunction.toString = function () { return "function () {\\n [native code]\\n}"; };' + + 'return boundFunction;' + + '};' + + '})();\n'; + + const expectedEvaluationResult: number = 1; + + let evaluationResult: number = 0; + + before(() => { + const code: string = readFileAsString(__dirname + '/fixtures/input.js'); + + let obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, { + ...baseOptions, + selfDefending: true + }).getObfuscatedCode(); + obfuscatedCode = beautifyCode(obfuscatedCode, 'space'); + obfuscatedCode = `${reserializingEngineSimulation}${obfuscatedCode}`; + + return evaluateInWorker(obfuscatedCode, redosEvaluationTimeout).then((result: string | null) => { + if (!result) { + return; + } + + evaluationResult = parseInt(result, 10); + }); + }); + + it('should not enter code in infinity loop and evaluate correctly', () => { + assert.equal(evaluationResult, expectedEvaluationResult); + }); + }); }); diff --git a/test/index.spec.ts b/test/index.spec.ts index ebf3a2525..7bbe93f4c 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -149,6 +149,7 @@ import './functional-tests/storages/string-array-transformers/string-array-stora * Performance tests */ import './performance-tests/JavaScriptObfuscatorPerformance.spec'; +import './performance-tests/SelfDefendingRuntimePerformance.spec'; /** * Runtime tests diff --git a/test/performance-tests/SelfDefendingRuntimePerformance.spec.ts b/test/performance-tests/SelfDefendingRuntimePerformance.spec.ts new file mode 100644 index 000000000..c0312355c --- /dev/null +++ b/test/performance-tests/SelfDefendingRuntimePerformance.spec.ts @@ -0,0 +1,55 @@ +import { assert } from 'chai'; + +import { evaluateInWorker } from '../helpers/evaluateInWorker'; + +import { JavaScriptObfuscator } from '../../src/JavaScriptObfuscatorFacade'; + +describe('SelfDefending runtime performance', function () { + const reserializingEngineSimulation: string = + '(function () {' + + 'var nativeToString = Function.prototype.toString;' + + 'Function.prototype.toString = function () {' + + 'return nativeToString.apply(this, arguments).replace(/([{;])/g, "$1\\n ");' + + '};' + + '})();\n'; + + const runtimeBudget: number = 5000; + + let obfuscatedCode: string, + elapsed: number = runtimeBudget, + evaluationResult: number = 0; + + this.timeout(30000); + + before(() => { + const code: string = 'globalThis.selfDefendingResult = (function () { return 1; })();'; + + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + compact: true, + selfDefending: true, + stringArray: false, + target: 'node' + }).getObfuscatedCode(); + obfuscatedCode = `${reserializingEngineSimulation}${obfuscatedCode};globalThis.selfDefendingResult;`; + + const startTime: number = Date.now(); + + return evaluateInWorker(obfuscatedCode, runtimeBudget).then((result: string | null) => { + elapsed = Date.now() - startTime; + + if (!result) { + return; + } + + evaluationResult = parseInt(result, 10); + }); + }); + + it('should skip the beautifier trap and evaluate correctly (not hang) on a re-serializing engine', () => { + assert.equal(evaluationResult, 1); + }); + + it('should evaluate well under the ReDoS-trap runtime budget', () => { + assert.isBelow(elapsed, runtimeBudget); + }); +}); From a3df841839d4549ea521cf3e5dbbb01d67b0f5dc Mon Sep 17 00:00:00 2001 From: Timofey Kachalov Date: Thu, 9 Jul 2026 14:34:48 +0400 Subject: [PATCH 3/3] Fixed dropped parentheses around an `in` operator inside an arrow body in a `for`-init, producing unparsable output (#1426) --- CHANGELOG.md | 1 + package.json | 2 +- .../issues/fixtures/issue1419.js | 3 ++ .../functional-tests/issues/issue1419.spec.ts | 29 +++++++++++++++++++ test/index.spec.ts | 1 + yarn.lock | 28 ++++-------------- 6 files changed, 41 insertions(+), 23 deletions(-) create mode 100644 test/functional-tests/issues/fixtures/issue1419.js create mode 100644 test/functional-tests/issues/issue1419.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 322ca03e5..25f5d4c4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ v5.4.5 --- * Fixed `controlFlowFlattening` intermittently dropping arguments of a spread call (e.g. `foo(...args)`) when it reused a control flow wrapper of a same-arity plain call. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1423 * Fixed `selfDefending` making obfuscated code run several times slower on Bun/JavaScriptCore. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1421 +* Fixed dropped parentheses around an `in` operator inside an arrow body in a `for`-init, producing unparsable output. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1419 v5.4.4 --- diff --git a/package.json b/package.json index 2f3e5035d..107c8d02e 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "types": "typings/index.d.ts", "dependencies": { - "@javascript-obfuscator/escodegen": "2.4.1", + "@javascript-obfuscator/escodegen": "2.4.2", "@javascript-obfuscator/estraverse": "5.4.0", "@vercel/blob": ">=0.23.0", "acorn": "8.15.0", diff --git a/test/functional-tests/issues/fixtures/issue1419.js b/test/functional-tests/issues/fixtures/issue1419.js new file mode 100644 index 000000000..faeccdf84 --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue1419.js @@ -0,0 +1,3 @@ +for (a = (t, e) => (t in e) ? 0 : 1;;) { + break; +} diff --git a/test/functional-tests/issues/issue1419.spec.ts b/test/functional-tests/issues/issue1419.spec.ts new file mode 100644 index 000000000..10990eb72 --- /dev/null +++ b/test/functional-tests/issues/issue1419.spec.ts @@ -0,0 +1,29 @@ +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/1419 +// +describe('Issue #1419', () => { + describe('`in` operator inside an arrow concise body within a `for`-init', () => { + let obfuscatedCode: string; + + before(() => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1419.js'); + + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + compact: true + }).getObfuscatedCode(); + }); + + it('should produce syntactically valid code (keep the `in` parentheses in the `for`-init NoIn context)', () => { + assert.doesNotThrow(() => new Function(obfuscatedCode)); + }); + }); +}); diff --git a/test/index.spec.ts b/test/index.spec.ts index 7bbe93f4c..8c80b0687 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -88,6 +88,7 @@ import './functional-tests/issues/issue355.spec'; import './functional-tests/issues/issue419.spec'; import './functional-tests/issues/issue424.spec'; import './functional-tests/issues/issue437.spec'; +import './functional-tests/issues/issue1419.spec'; import './functional-tests/javascript-obfuscator/JavaScriptObfuscator.spec'; import './functional-tests/node-transformers/control-flow-transformers/block-statement-control-flow-transformer/BlockStatementControlFlowTransformer.spec'; import './functional-tests/node-transformers/control-flow-transformers/control-flow-replacers/binary-expression-control-flow-replacer/BinaryExpressionControlFlowReplacer.spec'; diff --git a/yarn.lock b/yarn.lock index 832bb6048..0ba02834c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -352,10 +352,10 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@javascript-obfuscator/escodegen@2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@javascript-obfuscator/escodegen/-/escodegen-2.4.1.tgz#6062541cb3027912e9304dd15f8a5ee5946de247" - integrity sha512-YrEJJDr4cb+pIQKWzHFoDlDkQzatcrNB6OhAD6iTSwiKwzZUMVdobwbOuLpF4EiLxUj0qP28Xl1saTHYzIPCLg== +"@javascript-obfuscator/escodegen@2.4.2": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@javascript-obfuscator/escodegen/-/escodegen-2.4.2.tgz#8a18dac7727e9ce873322207ca13db4841f73250" + integrity sha512-3N7EUdYxuldwvtOtZIlAxNwPW1qTUFwWt3PxNkik6hB1uSSXLXj9JMlSyUbTCXK3Osjn0Nu0s/aa4mbvkGouXQ== dependencies: "@javascript-obfuscator/estraverse" "^5.3.0" esprima "^4.0.1" @@ -4945,7 +4945,7 @@ string-template@1.0.0: resolved "https://registry.npmjs.org/string-template/-/string-template-1.0.0.tgz" integrity sha1-np8iM9wA8hhxjsN5oopWc+zKi5Y= -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -4963,15 +4963,6 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -5052,7 +5043,7 @@ stringz@2.1.0: dependencies: char-regex "^1.0.2" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -5066,13 +5057,6 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.2" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba"