Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Change Log
v5.8.1
---
* Fixed `transformObjectKeys` making default parameter objects shared across calls. Fixed https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1455
* Fixed `controlFlowFlattening` breaking short-circuit evaluation of `&&`, `||` and `??`. Fixed https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1456

v5.8.0
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ export class LogicalExpressionControlFlowReplacer extends ExpressionWithOperator
leftExpression: ESTree.Expression,
rightExpression: ESTree.Expression
): boolean {
if (this.expressionContainsProhibitedNodes(rightExpression)) {
if (
this.expressionContainsProhibitedNodes(rightExpression) ||
!this.isSafeToEvaluateEagerly(rightExpression)
) {
return true;
}

Expand All @@ -112,6 +115,22 @@ export class LogicalExpressionControlFlowReplacer extends ExpressionWithOperator
});
}

/**
* The right operand becomes an argument of the control flow storage call and is always evaluated,
* so short-circuit evaluation is lost. Only literals are safe to evaluate eagerly: identifiers may throw
* (temporal dead zone, undeclared globals) and object expressions may throw or have side effects
*
* @param {Expression} expression
* @returns {boolean}
*/
private isSafeToEvaluateEagerly(expression: ESTree.Expression): boolean {
const nodeForCheck: ESTree.Node = NodeGuards.isUnaryExpressionNode(expression)
? NodeUtils.getUnaryExpressionArgumentNode(expression)
: expression;

return NodeGuards.isLiteralNode(nodeForCheck);
}

/**
* @param {Expression} expression
* @returns {boolean}
Expand Down
60 changes: 60 additions & 0 deletions test/functional-tests/issues/issue1456.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { assert } from 'chai';

import { NO_ADDITIONAL_NODES_PRESET } from '../../../src/options/presets/NoCustomNodes';

import { JavaScriptObfuscator } from '../../../src/JavaScriptObfuscatorFacade';

//
// https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1456
//
describe('Issue #1456', () => {
describe('`controlFlowFlattening` should keep short-circuit evaluation of logical expressions', () => {
const samples: { name: string; code: string; expectedResult: unknown }[] = [
{
name: 'Variant #1: `&&` with object expression that reads property of falsy left operand',
code: 'function size(img) { return img && { width: img.width }; } return size(null);',
expectedResult: null
},
{
name: 'Variant #2: `&&` with identifier in temporal dead zone',
code: 'function pick(ready) { return ready && late; } var result = pick(false); let late = 1; return result;',
expectedResult: false
},
{
name: 'Variant #3: `&&` with undeclared global identifier',
code: 'function pick(ready) { return ready && undeclaredGlobal1456; } return pick(false);',
expectedResult: false
},
{
name: 'Variant #4: `||` with side effect inside object expression',
code: 'var count = 0; function next(seen) { return seen || { n: ++count }; } next(true); return count;',
expectedResult: 0
},
{
name: 'Variant #5: `??` with unary expression over identifier in temporal dead zone',
code: 'function pick(value) { return value ?? !late; } var result = pick(1); let late = 1; return result;',
expectedResult: 1
}
];

samples.forEach(({ name, code, expectedResult }) => {
describe(name, () => {
let result: unknown;

before(() => {
const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, {
...NO_ADDITIONAL_NODES_PRESET,
controlFlowFlattening: true,
controlFlowFlatteningThreshold: 1
}).getObfuscatedCode();

result = new Function(obfuscatedCode)();
});

it('should not evaluate the skipped right operand', () => {
assert.strictEqual(result, expectedResult);
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ describe('LogicalExpressionControlFlowReplacer', function () {

describe('Variant #3 - single logical expression with unary expression', () => {
const controlFlowStorageCallRegExp: RegExp = new RegExp(
`var ${variableMatch} *= *${variableMatch}\\['\\w{5}'\\]\\(!${variableMatch}, *!${variableMatch}\\);`
`var ${variableMatch} *= *${variableMatch}\\['\\w{5}'\\]\\(!${variableMatch}, *!!\\[\\]\\);`
);

let obfuscatedCode: string;
Expand Down Expand Up @@ -140,5 +140,51 @@ describe('LogicalExpressionControlFlowReplacer', function () {
assert.match(obfuscatedCode, regExp);
});
});

// issue https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1456
describe('prohibited nodes Variant #2: identifier as right operand', () => {
const regExp: RegExp = new RegExp(
`var ${variableMatch} *= *!${variableMatch} *&& *!${variableMatch};`
);

let obfuscatedCode: string;

before(() => {
const code: string = readFileAsString(__dirname + '/fixtures/prohibited-nodes-identifier.js');

obfuscatedCode = JavaScriptObfuscator.obfuscate(code, {
...NO_ADDITIONAL_NODES_PRESET,
controlFlowFlattening: true,
controlFlowFlatteningThreshold: 1
}).getObfuscatedCode();
});

it("shouldn't replace logical expression with identifier as right operand", () => {
assert.match(obfuscatedCode, regExp);
});
});

// issue https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1456
describe('prohibited nodes Variant #3: object expression as right operand', () => {
const regExp: RegExp = new RegExp(
`var ${variableMatch} *= *${variableMatch} *&& *{'foo': *${variableMatch}};`
);

let obfuscatedCode: string;

before(() => {
const code: string = readFileAsString(__dirname + '/fixtures/prohibited-nodes-object-expression.js');

obfuscatedCode = JavaScriptObfuscator.obfuscate(code, {
...NO_ADDITIONAL_NODES_PRESET,
controlFlowFlattening: true,
controlFlowFlatteningThreshold: 1
}).getObfuscatedCode();
});

it("shouldn't replace logical expression with object expression as right operand", () => {
assert.match(obfuscatedCode, regExp);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
(function () {
var expression1 = true;
var expression2 = false;
var variable = !expression1 && !expression2;
var variable = !expression1 && !false;
})();
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
(function () {
var expression1 = true;
var expression2 = false;
var variable = !expression1 && !expression2;
})();
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(function () {
var expression1 = true;
var variable = expression1 && {foo: expression1};
})();
1 change: 1 addition & 0 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ import './functional-tests/issues/issue1419.spec';
import './functional-tests/issues/issue1437.spec';
import './functional-tests/issues/issue1448.spec';
import './functional-tests/issues/issue1455.spec';
import './functional-tests/issues/issue1456.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';
Expand Down
Loading