Skip to content

controlFlowFlattening rewrites let/const to var inside functions passed to wrapped calls #1471

Closed

Description

@trackoor

Expected Behavior

Obfuscating with controlFlowFlattening: true should preserve let and const declarations inside functions or arrow functions passed as arguments to function calls. In the example below, the obfuscated code should print 0,1,2, matching the original code where let j = i creates a fresh block-scoped binding on each loop iteration.

Current Behavior

Enabling controlFlowFlattening rewrites let and const declarations to var inside function or arrow arguments passed to wrapped calls whenever var is the most frequent declaration kind in the input. In the example below, the obfuscated code prints 2,2,2 instead of 0,1,2 because let j = i inside the callback is converted to var, collapsing all loop iterations onto a single function-scoped binding.

This appears to happen because CallExpressionControlFlowStorageCallNode embeds the user argument AST into its custom node structure, and CustomCodeHelperFormatter.formatStructure then rewrites every VariableDeclaration in the subtree to kind = 'var' when PrevailingKindOfVariablesAnalyzer selects var.

Steps to Reproduce

  1. Obfuscate a script where var is the prevailing variable declaration kind and a function/arrow callback containing a per-iteration let binding in a loop is passed as an argument to a call expression, with controlFlowFlattening: true and controlFlowFlatteningThreshold: 1.
  2. Execute the obfuscated code.
  3. Observe that let inside the callback was rewritten to var, changing the output from 0,1,2 to 2,2,2.

JavaScript Obfuscator Edition

  • JavaScript Obfuscator Open Source

Your Environment

  • Obfuscator version used: 5.10.0
  • Node version used: v24.14.1

Stack trace

N/A (wrong runtime output, no exception thrown)

Minimal working example that will help to reproduce issue

const JavaScriptObfuscator = require('javascript-obfuscator');

const code = `
function main() {
  var run = (f) => f();
  return run(() => {
    var fs = [];
    for (var i = 0; i < 3; i++) {
      let j = i;
      fs.push(() => j);
    }
    return fs.map((g) => g()).join();
  });
}
console.log(main());
`;

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

eval(out);

Credit: https://ucsbplse.github.io/

Activity

  1. sanex3339 commented on Nov 12, 2026

    @sanex3339
    Member

    Released as 5.10.1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions