Deobfuscator Test

JavaScript deobfuscators, tested: what they recover and what they cannot

We protected one small program at five levels with our own engine, and at two levels with the open-source javascript-obfuscator, then ran four deobfuscators against every file. Here is what came back, including the results that are unflattering to us.

Short Version

Strings come back. Names do not.

An evaluating deobfuscator recovered all nine test strings even from our strongest preset. No tool, at any level, recovered one original local variable name.

Toolswebcrack, synchrony, restringer, js-beautify
Inputs7 protected files + 3 unpacked
Run2026-10-29, versions listed below
The Test

One program, nine strings worth stealing

A licence check and a price quote: a licensing URL, a checksum salt, the key prefix, three feature flags, a region and an error message — nine string literals in all. Every protected file printed exactly what the original printed before we deobfuscated it.

sample.js (excerpt)
function licenseEndpoint(region) {
  return "https://licensing.acme-internal.example/v3/" + region + "/activate";
}
function checksum(body) {
  var hash = 0, salt = "acme-license-salt-2026";
  ...
}
function featureFlags(plan) {
  if (plan === "business") return ["sso-login", "audit-export", "priority-queue"];
  ...

The protected files

  • Free — local names replaced, strings encoded.
  • Free + Move Strings — strings also lifted into one table.
  • Standard — plus the Compressor, which packs the code into a self-extracting wrapper.
  • Balanced — plus Encrypt Strings, deep obfuscation, code transposition and Flat Transform.
  • Maximum — plus global renaming, member moving and dead code.
  • javascript-obfuscator 5.9 — its default and high-obfuscation presets, for comparison.
Results

Strings recovered, out of nine

“Unpacked” rows are the same file after one manual step anyone can take in DevTools: catching the code the Compressor hands to the JavaScript engine. “Breaks” means the deobfuscated code no longer printed the original result.

ProtectionAs shippedjs-beautifywebcracksynchronyrestringer
Free00999
Free + Move Strings00999
Standard00009
  Standard, unpacked99999
Balanced00009, breaks
  Balanced, unpacked0000, breaks9, breaks
Maximum00009
  Maximum, unpacked000error0
javascript-obfuscator, default889timeout9
javascript-obfuscator, high009timeouttimeout
Reading The Table

Four things the numbers say

1. Encoded strings are a speed bump for people, not tools

A beautifier leaves \x68\x74\x74\x70… as it is, but webcrack, synchrony and restringer all turned the free level’s strings back into text instantly. Encoding stops someone searching the file; it does not stop a tool.

2. Packing stops static tools, not people

webcrack and synchrony read code without running it, and recovered nothing from the Standard file. One unpacking step later the strings were plain text. restringer, which runs code in a sandbox, did not need the manual step at all.

3. Encrypted strings held against static tools, not against evaluation

With Encrypt Strings and flattening (Balanced, Maximum), webcrack and synchrony recovered nothing even from the unpacked code. restringer recovered all nine strings from the shipped Balanced and Maximum files by running their decoders — for Balanced, producing code that no longer ran.

4. Names never came back

Across every file and tool, zero of the original local variable names reappeared, and at Maximum none of the renamed top-level function names did either. What deobfuscation returns is working code made of meaningless names; understanding it is still a person’s job, and that is where obfuscation costs an attacker time.

What Came Back

Free-level output, before and after webcrack

Protected (free)
function licenseEndpoint(j){return "\x68\x74\x74\x70\x73\x3A\x2F\x2F\x6C\x69\x63\x65\x6E\x73\x69\x6E\x67\x2E\x61\x63\x6D\x65\x2D\x69\x6E\x74\x65\x72\x6E\x61\x6C\x2E\x65\x78\x61\x6D\x70\x6C\x65\x2F\x76\x33\x2F"+ j+ "\x2F\x61\x63\x74\x69\x76\x61\x74\x65"}...
After webcrack
function licenseEndpoint(j) {
  return "https://licensing.acme-internal.example/v3/" + j + "/activate";
}
function checksum(a) {
  var b = 0;
  var d = "acme-license-salt-2026";
  var e = a + d;
  ...

licenseEndpoint keeps its name because it is a global other scripts may call; turn on global renaming (Maximum) and it becomes a one-letter name no tool restored. region, salt and text are gone for good: j, d, e.

If You Need To Deobfuscate

Which deobfuscator to use

ToolHow it worksBest at, in this testWatch out
webcrack 2.16Static transforms, plus a sandbox for string decodersjavascript-obfuscator output (all strings, even the high preset); encoded stringsRecovered nothing from packed code
restringer 2.3Evaluates expressions in an isolated-vm sandboxMost strings overall, including behind packers and string encryptionOutput sometimes stops working; slowest; timed out on javascript-obfuscator high
synchrony 2.4 (deobfuscator on npm)Static transforms aimed at javascript-obfuscatorEncoded stringsTimed out on both javascript-obfuscator 5.9 files (“no decoder”)
js-beautify 2.0Reformatting onlyMaking minified code readableDecodes nothing

webcrack and restringer run parts of the input to resolve strings. Treat deobfuscating code you do not trust like running it: use a disposable machine or container. Trying AI instead? We tested that separately in can AI reverse-engineer obfuscated JavaScript?

If You Protect Code

What to take from this

  • Never put a secret in a string. Every string in this test was recovered by at least one tool. Keys and passwords belong on the server — why you cannot hide an API key in JavaScript.
  • Value lies in names and structure. Renaming is the one transform no tool undid. Global renaming at Maximum extends it to the names other code does not need.
  • Encrypt Strings raises the bar to evaluating tools, which are slower, less reliable and riskier to run.
  • For logic that must not be lifted, look at VM bytecode protection, which replaces your code with bytecode for a generated interpreter. It was not part of this test.
Method

Reproducing the run

  • Versions: webcrack 2.16.0, deobfuscator (synchrony) 2.4.6, restringer 2.3.0, js-beautify 2.0.3, javascript-obfuscator 5.9.0, Node.js 24.15.
  • Our files came from our engine with a fixed seed and the option sets of the CLI presets; javascript-obfuscator ran with seed 7 and its self-defending and debug protection off.
  • Each tool ran in its own process with a 90-second limit. A string counts as recovered when it appears verbatim in the output.
  • “Unpacked” files hold the code captured at the point the Compressor passes it to eval or Function.
Frequently Asked

JavaScript deobfuscation questions

Can obfuscated JavaScript be deobfuscated?

Partly. In our test, deobfuscators recovered every string literal from most protection levels, because the code that decodes them ships with the page. None of them recovered a single original local variable name: renaming is one-way. What comes back is working code with meaningless names, which still takes a person time to understand. More in is JavaScript obfuscation reversible?

What is the best JavaScript deobfuscator?

It depends on what produced the code. webcrack recovered every string from javascript-obfuscator’s high-obfuscation preset and from simply encoded strings. restringer, which evaluates code in a sandbox, recovered the most overall, including strings behind a packer, but sometimes produced code that no longer ran. js-beautify only reformats.

Is a deobfuscator safe to run on code I do not trust?

Be careful. webcrack and restringer evaluate parts of the input to resolve strings (in an isolated-vm sandbox), so treat deobfuscating hostile code like running it: use a disposable machine or container.

Does this mean obfuscation is pointless?

No, but it shows what it is for. String hiding stops searching and casual reading, not tools. Lost names and restructured code are what cost an attacker time. Secrets such as API keys must never be in client code at all.

Try It Yourself

Protect a file, then point a deobfuscator at it

The online tool previews every preset on your own code, free and without an account.