Repository navigation
Description
Expected Behavior
Obfuscated code keeps short-circuit evaluation: in a && b the right operand is not evaluated when a is falsy (and likewise for || and ??).
Current Behavior
With controlFlowFlattening: true, inside a block-bodied function, LogicalExpressionControlFlowReplacer rewrites a && b into a call to a control-flow storage helper, S.fn(a, b), whenever both operands are a literal, an identifier or an object literal (optionally under unary operators) and the right operand contains no call and no computed member expression. Call arguments are evaluated before the call, so the right operand now always runs:
img && { width: img.width }throwsTypeErrorwhenimgisnullready && latethrowsReferenceErrorwhilelate(alet) is in its temporal dead zone, or whenlateis an undeclared globalseen || { seen, n: ++count }increments on every call; getters andvalueOfrun
Each eligible site is replaced with probability controlFlowFlatteningThreshold. Without a seed, which sites break changes from run to run.
checkForProhibitedExpressions treats identifiers and object literals as side-effect free, but evaluating them early can throw or have effects. The helper would need to take the right operand lazily (for example S.fn(a, () => b)), or the replacer would need to be limited to operands that are safe to evaluate early (literals only).
Steps to Reproduce
// node repro.js
const JavaScriptObfuscator = require('javascript-obfuscator');
const code = `
function size(img) { return img && { width: img.width }; }
function pick(ready) { return ready && late; }
var count = 0;
function next(seen) { return seen || { seen: seen, n: ++count }; }
function report(name, fn) { try { console.log(name, JSON.stringify(fn())); } catch (e) { console.log(name, 'throws', e.constructor.name + ': ' + e.message); } }
report('size(null) ', function () { return size(null); });
report('pick(false) ', function () { return pick(false); });
report('next(true) ', function () { next(true); return count; });
let late = 1;
`;
const options = { controlFlowFlattening: true, controlFlowFlatteningThreshold: 1, seed: 1 };
console.log('--- plain');
new Function(code)();
console.log('--- obfuscated');
new Function(JavaScriptObfuscator.obfuscate(code, options).getObfuscatedCode())();Output (5.5.0 and 5.8.0):
--- plain
size(null) null
pick(false) false
next(true) 0
--- obfuscated
size(null) throws TypeError: Cannot read properties of null (reading 'width')
pick(false) throws ReferenceError: Cannot access 'late' before initialization
next(true) 1
size after obfuscation (stringArray: false, compact: false):
function size(_0x3f434e) {
var _0x2c7393 = {
'PSBht': function (_0x26868c, _0x4eb5be) {
return _0x26868c && _0x4eb5be;
}
};
return _0x2c7393['PSBht'](_0x3f434e, { 'width': _0x3f434e['width'] });
}The same happens in methods, getters, class field initialisers inside functions, async functions, generators and block-bodied arrows. Expression-bodied arrows, parameter defaults and top-level code are not affected (FunctionControlFlowTransformer only transforms block bodies).
JavaScript Obfuscator Edition
- JavaScript Obfuscator Open Source (npm
javascript-obfuscator)
Your Environment
- Obfuscator version used: 5.5.0, 5.8.0
- Node version used: 25.4.0
Workaround
Before obfuscating, rewrite each such right operand as (0, b). The replacer declines sequence expressions.
Activity
sanex3339 commentedon Sep 30, 2026
Will be released later today as 5.8.1