Guide

Recognizing JavaScript Obfuscation: A Deobfuscation Primer

April 20, 2026
10 min read
Recognizing JavaScript Obfuscation: A Deobfuscation Primer

Introduction

JavaScript obfuscation is the practice of deliberately transforming readable source code into a functionally equivalent but intentionally confusing version. While developers sometimes use it to protect proprietary logic, malicious actors (adware operators, credential stealers, and fraudulent ad platforms) rely on it to hide what their scripts actually do from security researchers, browser extensions, and casual inspection.

Before you can deobfuscate any script, you need to recognize what was done to it. Obfuscated code in the wild rarely uses a single technique; it layers several of them together, and each layer has to be peeled back in the right order. This article covers the six most common obfuscation techniques you will encounter. In the follow-up article, we will apply this knowledge directly to a real-world obfuscated script.

This is Part 1 of a two-part series. Part 2 applies these techniques to a real-world obfuscated script.

Obfuscation Methods

1. Minification

Minification is the simplest form of obfuscation, though technically it is a side effect of build optimization rather than a deliberate obfuscation technique. All whitespace, comments, and meaningful variable names are stripped out, reducing file size for faster load times. Attackers leverage it as a first layer because it removes the narrative structure that helps humans read code (line breaks, indentation, and descriptive names all disappear).

Original:

function checkUserIsBot(navigator) {
    if (navigator.webdriver === true) {
        return true;
    }
    return false;
}

After minification:

function c(n){return n.webdriver===true;}

How to recognize it: The code is one long line or has no whitespace. Variable and function names are one or two characters.

How to reverse it: Any code editor or online tool (Prettier, JS Beautifier) can re-indent minified code in seconds. This is always the first step; make the code readable before doing anything else.

2. Hex / Unicode / Base64 Encoding

String literals and property names are replaced with their hex or Unicode escape equivalents. The JavaScript engine interprets them identically to the original strings, but they appear as noise to a human reader. This technique specifically targets string inspection; if a security analyst searches a script for suspicious keywords like eval, fetch, or a tracking URL, encoded strings will evade that search.

These three lines do exactly the same thing:

console.log("hello");
console["\x6c\x6f\x67"]("\x68\x65\x6c\x6c\x6f");
console["\u006c\u006f\u0067"]("\u0068\u0065\u006c\u006c\u006f");

How to recognize it: You will see \x or \u sequences inside strings, or suspiciously long base64-looking strings assigned to variables. How to reverse it: Browser DevTools can decode these on the fly; paste the string into the console and JavaScript will evaluate it. For bulk decoding, tools like CyberChef handle all three encodings. In a deobfuscation workflow, you replace the encoded literals with their decoded equivalents before moving on.

3. String Array + Index Lookup

All string literals in the script are extracted and collected into a single array declared at the top of the file. Every location where a string originally appeared now references the array by index instead. This alone makes the code hard to follow, but attackers frequently go further: the array is shuffled or rotated by a self-executing function before the main code runs, breaking any direct index-to-string mapping.

Original:

console.log("Hello World");

After transformation:

// String array — often shuffled at runtime
var _0xabc = ['Hello', 'World', 'log', 'console'];

// Usage in code
_0xabc[3][_0xabc[2]](_0xabc[0] + " " + _0xabc[1]);

How to recognize it: A large array of strings near the top of the file, followed by a self-executing function that manipulates it, followed by code that accesses everything through array indices rather than direct strings.

How to reverse it: You need to let the rotation function execute first to get the final state of the array, then substitute each _0xabc[n] reference with its resolved string value. Tools like de4js and synchrony automate this step.

4. String Interleaving / Character Shuffling

Rather than encoding strings, this technique splits each string and interleaves its characters according to an index-based rule. A small decode function embedded in the script reassembles them at runtime. The result is that no sensitive string (a tracking endpoint, an API key, a suspicious domain) ever appears in plain text in the source.

The decode function:

const n = t => t.split('').reduce((n, t, e) => e % 2 ? n + t : t + n, '');

Usage:

n('p?hzpo.nuefiad/=') // → '/afu.php?zoneid='

To understand what this function does, trace through it character by character.

Input: "p?hzpo.nuefiad/="

Rule: even index → character goes to the FRONT of the accumulator
      odd index  → character goes to the BACK

reduce() starts with accumulator n = ""
e=0  (even): t + n → "p" + ""                = "p"
e=1  (odd): n + t → "p" + "?"                = "p?"
e=2  (even): t + n → "h" + "p?"              = "hp?"
e=3  (odd): n + t → "hp?" + "z"              = "hp?z"
e=4  (even): t + n → "p" + "hp?z"            = "php?z"
e=5  (odd): n + t → "php?z" + "o"            = "php?zo"
e=6  (even): t + n → "." + "php?zo"          = ".php?zo"
e=7  (odd): n + t → ".php?zo" + "n"          = ".php?zon"
e=8  (even): t + n → "u" + ".php?zon"        = "u.php?zon"
e=9  (odd): n + t → "u.php?zon" + "e"        = "u.php?zone"
e=10 (even): t + n → "f" + "u.php?zone"      = "fu.php?zone"
e=11 (odd): n + t → "fu.php?zone" + "i"      = "fu.php?zonei"
e=12 (even): t + n → "a" + "fu.php?zonei"    = "afu.php?zonei"
e=13 (odd): n + t → "afu.php?zonei" + "d"    = "afu.php?zoneid"
e=14 (even): t + n → "/" + "afu.php?zoneid"  = "/afu.php?zoneid"
e=15 (odd): n + t → "/afu.php?zoneid" + "="  = "/afu.php?zoneid="

Result: "/afu.php?zoneid="

The encoding process is performed offline by the attacker using the inverse operation. What you see in the script is only the decoder and the already-shuffled strings.

How to recognize it: A short, cryptic arrow function operating on strings with split, reduce, or similar array methods near the top of the file. All string arguments to it will look like scrambled noise.

How to reverse it: Identify the decode function and call it directly in the browser console on every obfuscated string. Since the function is already in the script, you are just using the attacker's own code against itself. This is one of the most satisfying steps in a deobfuscation workflow.

5. Variable Name Mangling

Meaningful identifiers (such as checkIsBot, sendFingerprint, trackingEndpoint) are replaced with single letters, random alphanumeric sequences, or visually confusing lookalikes (using characters like l, 1, I together). This technique does not change program logic at all; it only destroys the semantic information that helps a human understand what the code is doing. It is almost always combined with other techniques.

Original:

var screenWidth = window.screen.width;
var screenHeight = window.screen.height;
var timezone = new Date().getTimezoneOffset();

After mangling:

var rR = window.screen.width;
var rk = window.screen.height;
var G3 = new Date().getTimezoneOffset();

How to recognize it: Every variable and function name is meaningless on its own. You can only understand a variable's purpose by tracing how it is assigned and where it is used.

How to reverse it: This is primarily a manual process, you rename variables as you understand them. Some deobfuscation tools attempt automated renaming based on usage context. In practice, you build a rename map as you read the code and apply it progressively. After one pass through a well-structured deobfuscator, the code structure becomes clear enough to rename manually.

6. Webpack / Module Bundler Wrapping

Webpack and similar bundlers package all JavaScript modules of an application into a single file. Legitimate sites use this for performance. Attackers exploit the pattern deliberately because analysts often dismiss Webpack bundles as routine framework output without inspecting the individual modules inside.

In a Webpack bundle, all modules are stored in a dictionary keyed by hex numbers. A central loader function accepts a key and returns the corresponding module's exports. Every inter-module dependency becomes an opaque V(0x266) call; you have to look up the hex key in the dictionary to understand what is actually being loaded.

// Module dictionary
var G = {
    0x266: (B, W, L) => {
        var w = L(0x103a); // loads another module
        B['exports'] = function(g) { return 'function' == typeof g; };
    },
    0x103a: (B, W, L) => {
        var W = typeof document !== 'undefined' && document['all'];
        B['exports'] = { all: W, IS_HTMLDDA: typeof W === 'undefined' };
    }
};

// Loader function
function V(B) {
    if (s[B]) return s[B].exports; // cache hit
    var L = s[B] = { exports: {} };
    G[B](L, L.exports, V);
    return L.exports;
}

Every V(0x266) call resolves to whatever G[0x266] exports. To follow the execution chain, you trace loader calls back to their dictionary entries and read each module in isolation.

How to recognize it: A large object literal at the top of the file with hex-keyed entries, each containing an arrow function with a consistent signature like (B, W, L). A loader function that takes a hex key and returns exports.

How to reverse it: Work module by module. Start from the entry point (usually the first loader call in the script), follow each V(hex) reference to its module, understand what it exports, and mentally substitute it. Tools like webpack-deobfuscator can partially automate this, but manual tracing is often necessary for heavily modified bundles.

Putting It Together

In the wild, you will rarely encounter these techniques in isolation. A real-world malicious script might be Webpack-bundled, with all string literals collected into a rotated array, encoded in hex, and with every variable mangled on top of that. Each layer has to be addressed in sequence, generally from outermost to innermost:

  1. Beautify the minified code first
  2. Decode any hex/unicode/base64 literals
  3. Resolve the string array by executing the rotation function
  4. Identify and call any custom decode functions (like the interleaving decoder)
  5. Rename variables as their purpose becomes clear
  6. Trace Webpack module boundaries to understand program structure

In the next article, we will go through this exact workflow on a real obfuscated script pulled from the wild, applying each of these techniques in sequence until the script's true behavior is fully exposed.