diff --git a/CHANGELOG.md b/CHANGELOG.md index 839cef024..2956b9345 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ Change Log +v5.4.3 +--- +* Fixed `controlFlowFlattening` occasionally dropping the `?.` short-circuit on `foo?.(arg)` calls, causing `TypeError: is not a function`. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1408 + v5.4.2 --- * Fixed obfuscated code hanging in Bun when `selfDefending` is enabled. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1404 diff --git a/package.json b/package.json index 0612d2257..71bad57d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-obfuscator", - "version": "5.4.2", + "version": "5.4.3", "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 a3d17e589..c02b59c17 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,7 +64,11 @@ export class CallExpressionControlFlowReplacer extends AbstractControlFlowReplac const isChainExpressionParent = NodeGuards.isChainExpressionNode(parentNode); - const replacerId: number = callExpressionNode.arguments.length; + // 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 callExpressionFunctionCustomNode: ICustomNode> = this.controlFlowCustomNodeFactory(ControlFlowCustomNode.CallExpressionFunctionNode); const expressionArguments: (ESTree.Expression | ESTree.SpreadElement)[] = callExpressionNode.arguments; 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 ea3b91a2f..0259d32d0 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 @@ -248,5 +248,33 @@ describe('CallExpressionControlFlowReplacer', function () { assert.match(obfuscatedCode, controlFlowStorageNodeRegExp); }); }); + + describe('Variant #8 - optional `?.()` call must not reuse a non-optional wrapper (issue #1408)', () => { + const samplesCount: number = 200; + + it('should preserve the `?.` short-circuit on every obfuscation when an optional call and a non-optional call share the same arity', () => { + const code: string = readFileAsString( + __dirname + '/fixtures/issue-1408-mixed-optional-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, + deadCodeInjection: true, + deadCodeInjectionThreshold: 1 + }).getObfuscatedCode(); + + const result: unknown = eval(obfuscatedCode); + + assert.strictEqual( + result, + 'ok', + `iteration ${i}: optional ?.() call lost its short-circuit (obfuscated code threw or returned wrong value)` + ); + } + }); + }); }); }); diff --git a/test/functional-tests/node-transformers/control-flow-transformers/control-flow-replacers/call-expression-control-flow-replacer/fixtures/issue-1408-mixed-optional-and-plain-calls.js b/test/functional-tests/node-transformers/control-flow-transformers/control-flow-replacers/call-expression-control-flow-replacer/fixtures/issue-1408-mixed-optional-and-plain-calls.js new file mode 100644 index 000000000..17e43b54c --- /dev/null +++ b/test/functional-tests/node-transformers/control-flow-transformers/control-flow-replacers/call-expression-control-flow-replacer/fixtures/issue-1408-mixed-optional-and-plain-calls.js @@ -0,0 +1,32 @@ +(function () { + function noise (n) { + var acc = 0; + for (var i = 0; i < n; i++) { + if (i % 2 === 0) { + acc += i; + } else { + acc ^= i; + } + } + return acc; + } + + function bump (v) { + return v + 1; + } + + function outer () { + var captured = undefined; + var inner = function (msg) { + captured?.(msg); + return 'ok'; + }; + return inner('payload'); + } + + noise(50); + bump(7); + noise(25); + bump(13); + return outer(); +})();