Skip to content

Quoted class constructor "constructor"() {} produces invalid code with splitStrings or stringArray #1464

Closed

Description

@mschoeffmann

Expected Behavior

A class constructor written with a string-literal key is valid JavaScript and is a real constructor:

class A {
  "constructor"(x) { this.x = x }
}

The obfuscated output should keep it valid. The key should stay the plain literal 'constructor', as it does with reservedStrings: ['^constructor$']. No computed or concatenated form keeps it a constructor.

Current Behavior

With splitStrings or stringArray enabled, the constructor key is rewritten in place and the output fails to parse.

splitStrings turns it into a concatenation:

class A{'co'+'ns'+'tr'+'uc'+'to'+'r'(_0x17215d){this['x']=_0x17215d;}}
// SyntaxError: Unexpected token '+'

stringArray turns it into a call:

class A{_0xd809b7(0x1dc)(_0x17215d){this['x']=_0x17215d;}}
// SyntaxError: Unexpected number

optionsPreset: 'medium-obfuscation' fails the same way because it enables splitStrings.

Other quoted keys are handled correctly. class A { "foo"() {} } becomes ['fo'+'o'](){}, which is valid. constructor is rightly kept out of that conversion to a computed key, but the string transform is still applied to it.

This occurs in real dependencies. @standard-server/peer, which the oRPC message-port adapter uses, ships "constructor"(callback) { ... }. In our Vite/Rolldown build, the invalid output produced an empty bundle without any error.

Steps to Reproduce

  1. npm install javascript-obfuscator
  2. Save the minimal example below as repro.cjs
  3. Run node repro.cjs

Output:

javascript-obfuscator 5.9.0, Node v24.21.0

input:
class A {
  "constructor"(x) { this.x = x }
}
console.log(new A(1).x)

--- without workaround (expected to fail) ---

options: {"splitStrings":true,"splitStringsChunkLength":2}
output:  class A{'co'+'ns'+'tr'+'uc'+'to'+'r'(_0x17215d){this['x']=_0x17215d;}}
result:  SyntaxError: Unexpected token '+'

options: {"stringArray":true,"stringArrayThreshold":1}
output:  class A{_0xd809b7(0x1dc)(_0x17215d){this['x']=_0x17215d;}}
result:  SyntaxError: Unexpected number

options: {"optionsPreset":"medium-obfuscation","disableConsoleOutput":false}
output:  class A{'constructo'+'r'(_0x4298d2){this['x']=_0x4298d2;}}
result:  SyntaxError: Unexpected token '+'

--- with workaround reservedStrings: ['^constructor$'] ---

options: {"splitStrings":true,"splitStringsChunkLength":2,"reservedStrings":["^constructor$"]}
output:  class A{'constructor'(_0x17215d){this['x']=_0x17215d;}}
result:  OK (prints 1)

options: {"stringArray":true,"stringArrayThreshold":1,"reservedStrings":["^constructor$"]}
output:  class A{'constructor'(_0x17215d){this['x']=_0x17215d;}}
result:  OK (prints 1)

options: {"optionsPreset":"medium-obfuscation","disableConsoleOutput":false,"reservedStrings":["^constructor$"]}
output:  class A{'constructor'(_0x4298d2){this['x']=_0x4298d2;}}
result:  OK (prints 1)

bug reproduced:   true
workaround works: true

JavaScript Obfuscator Edition

  • JavaScript Obfuscator Open Source

Your Environment

  • Obfuscator version used: 5.9.0 (also reproduced on 5.5.0)
  • Node version used: 24.21.0 (macOS)

Stack trace

Running the splitStrings output directly with node out.js:

out.js:1
...}(_0x358f,0xbde4a));class A{'co'+'ns'+'tr'+'uc'+'to'+'r'(_0x17215d){this['x']=_0x17215d;}}console['lo'+'g'](new A(0x1)['x']);...
                                                                                                ^

SyntaxError: Unexpected token '+'
    at wrapSafe (node:internal/modules/cjs/loader:1861:18)
    at Module._compile (node:internal/modules/cjs/loader:1903:20)
    at Object..js (node:internal/modules/cjs/loader:2060:10)

Minimal working example that will help to reproduce issue

// repro.cjs
// Quoted class constructor `"constructor"() {}` produces invalid code
// with `splitStrings` or `stringArray`.
const vm = require('node:vm')
const JavaScriptObfuscator = require('javascript-obfuscator')
const { version } = require('javascript-obfuscator/package.json')

const source = `class A {
  "constructor"(x) { this.x = x }
}
console.log(new A(1).x)`

const cases = [
  { splitStrings: true, splitStringsChunkLength: 2 },
  { stringArray: true, stringArrayThreshold: 1 },
  { optionsPreset: 'medium-obfuscation', disableConsoleOutput: false },
]

// Workaround: exclude the string from transformation
const workaround = { reservedStrings: ['^constructor$'] }

function run(options) {
  const code = JavaScriptObfuscator.obfuscate(source, {
    ...options,
    seed: 1,
  }).getObfuscatedCode()

  const logs = []
  let result
  try {
    vm.runInNewContext(code, {
      console: { log: (...args) => logs.push(args.join(' ')) },
    })
    result =
      logs.join('\n') === '1'
        ? 'OK (prints 1)'
        : `WRONG OUTPUT: ${JSON.stringify(logs)}`
  } catch (error) {
    result = `${error.name}: ${error.message}`
  }

  const classPart = code.match(/class A\{.*?\}\}/)?.[0] ?? '(class not found)'
  console.log(`options: ${JSON.stringify(options)}`)
  console.log(`output:  ${classPart}`)
  console.log(`result:  ${result}\n`)
  return result.startsWith('OK')
}

console.log(`javascript-obfuscator ${version}, Node ${process.version}\n`)
console.log(`input:\n${source}\n`)

console.log('--- without workaround (expected to fail) ---\n')
const failing = cases.map((options) => run(options))

console.log("--- with workaround reservedStrings: ['^constructor$'] ---\n")
const fixed = cases.map((options) => run({ ...options, ...workaround }))

const bugReproduced = failing.every((ok) => !ok)
const workaroundWorks = fixed.every((ok) => ok)
console.log(`bug reproduced:   ${bugReproduced}`)
console.log(`workaround works: ${workaroundWorks}`)
process.exitCode = bugReproduced && workaroundWorks ? 0 : 1

Activity

  1. sanex3339 commented on Nov 2, 2026

    @sanex3339
    Member

    Released as 5.9.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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions