Reversing Obfuscated JavaScript
Client-side JavaScript ships the whole program to the reader — there is no
stripped binary here, only code deliberately made unreadable. That changes the
job: you are almost never recovering missing information, you are undoing a
transformation. Identify the transformation first, reverse it with the tool
built for it, and fall back to hand-written AST passes only for the custom
layer no off-the-shelf tool knows.
When to Use
- A page or extension ships a large minified/bundled
.js you need to read
- Code is full of
_0x1234 identifiers, a big string array, and a rotation IIFE
at the top — the obfuscator.io signature
- A
.js.map sourcemap is reachable, or webpack left //# sourceMappingURL
- You must locate where a request signature, HMAC, token, or crypto key is
computed inside a running bundle
- Reproducing a client-side algorithm (an anti-bot signal, a licence check, a
paywall gate) from the shipped code
When NOT to Use
- React Native / Hermes mobile bundles (
index.android.bundle,
main.jsbundle, Hermes magic bytes) — use reversing-react-native-apps.
- A custom bytecode VM or virtualized-JS obfuscator where control flow is
interpreted, not just flattened — that is devirtualization; use
unpacking-protected-binaries for the VM-lifting approach.
- Malicious JS in a compromised site or npm package where the goal is IOCs
and behaviour, not readability — use
hunting-web-backdoors for planted web
payloads and auditing-supply-chain for a malicious package.
- Testing the web app itself (the endpoints the JS calls, XSS, auth) — use
testing-web-applications; come back here only to recover a client-side
algorithm it needs.
- WASM modules loaded by the page — that is
analyzing-binaries territory.
Identify the Layer Before Touching a Tool
Running the wrong deobfuscator produces plausible garbage. Read the first few
hundred bytes and classify:
| What you see |
Transformation |
Reverse it with |
| Short names, no whitespace, readable strings |
Minification only |
An unminifier / prettier + rename |
webpackChunk, a module map {123: function(e,t,n){…}} |
Webpack/Rollup bundling |
Unbundle to per-module files |
//# sourceMappingURL=… or a reachable .map |
Nothing — the source is right there |
Sourcemap recovery |
_0x hex names + one big string array + a rotation IIFE |
obfuscator.io string-array |
webcrack / synchrony / restringer |
Nested ternaries, while(true){switch(_0x..)} dispatcher |
Control-flow flattening |
AST pass to relink the switch |
debugger in a setInterval, self-defending function |
Anti-debug / self-defense |
Strip the guard before other passes |
Most real bundles are layered: webpack on the outside, obfuscator.io on a
few modules, a hand-rolled string cipher on the one function that matters.
Peel outermost first.
Sourcemaps: Try This First, Always
A reachable sourcemap ends the job before it starts — it contains the original,
pre-transform source. Check for it every time, because a large fraction of
"obfuscated" production bundles ship or leak one:
- The
//# sourceMappingURL= comment at the bundle's end, and the sibling
.js.map even when the comment was stripped (try <bundle>.map).
- Webpack dev artifacts and source in
webpack:// paths inside the map.
- Recover files with a sourcemap consumer —
unwebpack-sourcemap or a short
source-map script walks sourcesContent back to a directory tree.
Treat a leaked production sourcemap as a finding in its own right when you are
assessing the app, not just a convenience.
Unbundling and Deobfuscation
webcrack is the first tool for a bundle, because it does three of the
layers at once: unminify, unpack webpack/browserify into per-module files, and
undo obfuscator.io string-array and control-flow obfuscation. Run it, then read
the module tree it produces rather than the single blob.
For a bundle that is only obfuscator.io, a dedicated deobfuscator is often
cleaner: synchrony or restringer both resolve the string array,
reverse the rotation, inline the decoder calls, and flatten the trivial control
flow. Compare their output — they fail on different edge cases.
When no tool fully handles the custom layer, write an AST pass with Babel:
parse to an AST, @babel/traverse to find the pattern (a specific decoder call,
a constant-folded expression, the flattening dispatcher), transform the nodes,
and regenerate. This is the durable skill — obfuscators change, but string-array
decoding, constant folding, and dead-branch elimination are the same AST
operations every time. Prototype the matcher in AST Explorer against the real
code before scripting it.
Two failure modes to expect:
- A deobfuscator that runs the code to "evaluate" the string decoder can
execute a payload. For untrusted or malicious bundles, do the string
resolution statically or in an isolated sandbox — never
eval an unknown
bundle on your host to read it.
- Self-defending / debug-protection re-obfuscates or spins a
debugger
loop when tampered with. Strip the guard function first (delete the node,
or override setInterval) or every downstream pass fights it.
Finding a Routine in a Live Bundle
When static reading is slow — a signing function buried in a megabyte of
modules — drive the running page with Chrome DevTools instead:
- Break on the behaviour, not the code. XHR/fetch breakpoints stop when the
signed request fires; the call stack then walks straight back through the
signing function. DOM and event-listener breakpoints do the same for
UI-triggered logic.
- Pretty-print in the Sources panel (
{}) makes a minified function
steppable without any offline work.
- Hook property access from the console —
Object.defineProperty or a
Proxy on the object whose method computes the value logs every call and
argument with a stack trace, so you see inputs and outputs without reading
the math.
- Override the file with a local, prettified,
console.log-instrumented
copy (DevTools Local Overrides) to watch the real values flow at runtime.
Runtime observation and static reading are complementary: DevTools tells you
which function matters; the AST work tells you what it computes so you can
reproduce it offline.
Scope and Authorization
Reading and deobfuscating a bundle you lawfully retrieved is analysis. Two
edges need care. Reproducing and replaying a request-signing routine against
the origin — the usual reason to reverse an anti-bot signal — acts against that
service and needs the same authorization as any other testing; keep it to
systems you are permitted to test, and see testing-web-applications. And a
sourcemap or bundle pulled from a third-party site is that party's code:
recovering it for a security assessment you are engaged to do is fine, lifting a
proprietary algorithm for reuse is a different matter.
Rationalizations to Reject
- "It's obfuscated, so it's slow manual work." Classify first — a reachable
sourcemap or a clean
webcrack run turns hours into minutes. Hand-reading is
the last resort, not the first move.
- "webcrack's output has a weird spot, so it failed." Layered obfuscation
means one tool clears the outer layers and leaves the custom inner one. That
residue is the interesting function; switch to an AST pass on it, don't
restart.
- "I'll just eval the string-decoder to get the plaintext." On an untrusted
bundle that runs attacker code on your host. Resolve strings statically or
sandboxed.
- "The variable names are gone, so the logic is gone." Minification and
obfuscation rename and reshape; they do not delete. Unlike a stripped native
binary, the full program is present — you are undoing a transform, not
reconstructing lost information.
- "DevTools is for debugging, not reversing." A fetch breakpoint plus a
property hook locates a signing routine faster than reading the bundle, and
gives you live inputs and outputs for free.
References
reversing-react-native-apps — Hermes/RN mobile JS bundles
unpacking-protected-binaries — VM-based / virtualized obfuscation
hunting-web-backdoors — malicious planted web JS
testing-web-applications — testing the endpoints a recovered algorithm calls
reviewing-cryptography — once a client-side crypto/signing routine is recovered
1---2name: reversing-obfuscated-javascript3description: Reverse engineer minified, bundled, and obfuscated browser/Node JavaScript — unpacking webpack chunks, recovering source from sourcemaps, undoing obfuscator.io string-array and control-flow obfuscation with webcrack/synchrony/restringer, and locating a signing or crypto routine in a live bundle via Chrome DevTools. Use when a page ships a huge minified bundle, when code is full of _0x hex identifiers and a rotated string array, when a .js.map is reachable, or when you must find where a request signature or token is computed.4verified: 2026-08-075---67# Reversing Obfuscated JavaScript89Client-side JavaScript ships the whole program to the reader — there is no10stripped binary here, only code deliberately made unreadable. That changes the11job: you are almost never recovering *missing* information, you are undoing a12transformation. Identify the transformation first, reverse it with the tool13built for it, and fall back to hand-written AST passes only for the custom14layer no off-the-shelf tool knows.1516## When to Use1718- A page or extension ships a large minified/bundled `.js` you need to read19- Code is full of `_0x1234` identifiers, a big string array, and a rotation IIFE20 at the top — the obfuscator.io signature21- A `.js.map` sourcemap is reachable, or webpack left `//# sourceMappingURL`22- You must locate where a request signature, HMAC, token, or crypto key is23 computed inside a running bundle24- Reproducing a client-side algorithm (an anti-bot signal, a licence check, a25 paywall gate) from the shipped code2627## When NOT to Use2829- **React Native / Hermes mobile bundles** (`index.android.bundle`,30 `main.jsbundle`, Hermes magic bytes) — use `reversing-react-native-apps`.31- **A custom bytecode VM or virtualized-JS obfuscator** where control flow is32 interpreted, not just flattened — that is devirtualization; use33 `unpacking-protected-binaries` for the VM-lifting approach.34- **Malicious JS in a compromised site or npm package** where the goal is IOCs35 and behaviour, not readability — use `hunting-web-backdoors` for planted web36 payloads and `auditing-supply-chain` for a malicious package.37- **Testing the web app itself** (the endpoints the JS calls, XSS, auth) — use38 `testing-web-applications`; come back here only to recover a client-side39 algorithm it needs.40- **WASM modules** loaded by the page — that is `analyzing-binaries` territory.4142## Identify the Layer Before Touching a Tool4344Running the wrong deobfuscator produces plausible garbage. Read the first few45hundred bytes and classify:4647| What you see | Transformation | Reverse it with |48| --- | --- | --- |49| Short names, no whitespace, readable strings | Minification only | An unminifier / prettier + rename |50| `webpackChunk`, a module map `{123: function(e,t,n){…}}` | Webpack/Rollup bundling | Unbundle to per-module files |51| `//# sourceMappingURL=…` or a reachable `.map` | Nothing — the source is *right there* | Sourcemap recovery |52| `_0x` hex names + one big string array + a rotation IIFE | obfuscator.io string-array | webcrack / synchrony / restringer |53| Nested ternaries, `while(true){switch(_0x..)}` dispatcher | Control-flow flattening | AST pass to relink the switch |54| `debugger` in a `setInterval`, self-defending function | Anti-debug / self-defense | Strip the guard before other passes |5556Most real bundles are **layered**: webpack on the outside, obfuscator.io on a57few modules, a hand-rolled string cipher on the one function that matters.58Peel outermost first.5960## Sourcemaps: Try This First, Always6162A reachable sourcemap ends the job before it starts — it contains the original,63pre-transform source. Check for it every time, because a large fraction of64"obfuscated" production bundles ship or leak one:6566- The `//# sourceMappingURL=` comment at the bundle's end, and the sibling67 `.js.map` even when the comment was stripped (try `<bundle>.map`).68- Webpack dev artifacts and source in `webpack://` paths inside the map.69- Recover files with a sourcemap consumer — `unwebpack-sourcemap` or a short70 `source-map` script walks `sourcesContent` back to a directory tree.7172Treat a leaked production sourcemap as a finding in its own right when you are73assessing the app, not just a convenience.7475## Unbundling and Deobfuscation7677**`webcrack` is the first tool for a bundle**, because it does three of the78layers at once: unminify, unpack webpack/browserify into per-module files, and79undo obfuscator.io string-array and control-flow obfuscation. Run it, then read80the module tree it produces rather than the single blob.8182For a bundle that is *only* obfuscator.io, a dedicated deobfuscator is often83cleaner: **`synchrony`** or **`restringer`** both resolve the string array,84reverse the rotation, inline the decoder calls, and flatten the trivial control85flow. Compare their output — they fail on different edge cases.8687When no tool fully handles the custom layer, **write an AST pass** with Babel:88parse to an AST, `@babel/traverse` to find the pattern (a specific decoder call,89a constant-folded expression, the flattening dispatcher), transform the nodes,90and regenerate. This is the durable skill — obfuscators change, but string-array91decoding, constant folding, and dead-branch elimination are the same AST92operations every time. Prototype the matcher in AST Explorer against the real93code before scripting it.9495Two failure modes to expect:9697- **A deobfuscator that runs the code to "evaluate" the string decoder** can98 execute a payload. For untrusted or malicious bundles, do the string99 resolution statically or in an isolated sandbox — never `eval` an unknown100 bundle on your host to read it.101- **Self-defending / debug-protection** re-obfuscates or spins a `debugger`102 loop when tampered with. Strip the guard function first (delete the node,103 or override `setInterval`) or every downstream pass fights it.104105## Finding a Routine in a Live Bundle106107When static reading is slow — a signing function buried in a megabyte of108modules — drive the running page with Chrome DevTools instead:109110- **Break on the behaviour, not the code.** XHR/fetch breakpoints stop when the111 signed request fires; the call stack then walks straight back through the112 signing function. DOM and event-listener breakpoints do the same for113 UI-triggered logic.114- **Pretty-print** in the Sources panel (`{}`) makes a minified function115 steppable without any offline work.116- **Hook property access** from the console — `Object.defineProperty` or a117 `Proxy` on the object whose method computes the value logs every call and118 argument with a stack trace, so you see inputs and outputs without reading119 the math.120- **Override the file** with a local, prettified, `console.log`-instrumented121 copy (DevTools Local Overrides) to watch the real values flow at runtime.122123Runtime observation and static reading are complementary: DevTools tells you124*which* function matters; the AST work tells you *what it computes* so you can125reproduce it offline.126127## Scope and Authorization128129Reading and deobfuscating a bundle you lawfully retrieved is analysis. Two130edges need care. Reproducing and *replaying* a request-signing routine against131the origin — the usual reason to reverse an anti-bot signal — acts against that132service and needs the same authorization as any other testing; keep it to133systems you are permitted to test, and see `testing-web-applications`. And a134sourcemap or bundle pulled from a third-party site is that party's code:135recovering it for a security assessment you are engaged to do is fine, lifting a136proprietary algorithm for reuse is a different matter.137138## Rationalizations to Reject139140- **"It's obfuscated, so it's slow manual work."** Classify first — a reachable141 sourcemap or a clean `webcrack` run turns hours into minutes. Hand-reading is142 the last resort, not the first move.143- **"webcrack's output has a weird spot, so it failed."** Layered obfuscation144 means one tool clears the outer layers and leaves the custom inner one. That145 residue *is* the interesting function; switch to an AST pass on it, don't146 restart.147- **"I'll just eval the string-decoder to get the plaintext."** On an untrusted148 bundle that runs attacker code on your host. Resolve strings statically or149 sandboxed.150- **"The variable names are gone, so the logic is gone."** Minification and151 obfuscation rename and reshape; they do not delete. Unlike a stripped native152 binary, the full program is present — you are undoing a transform, not153 reconstructing lost information.154- **"DevTools is for debugging, not reversing."** A fetch breakpoint plus a155 property hook locates a signing routine faster than reading the bundle, and156 gives you live inputs and outputs for free.157158## References159160- `reversing-react-native-apps` — Hermes/RN mobile JS bundles161- `unpacking-protected-binaries` — VM-based / virtualized obfuscation162- `hunting-web-backdoors` — malicious planted web JS163- `testing-web-applications` — testing the endpoints a recovered algorithm calls164- `reviewing-cryptography` — once a client-side crypto/signing routine is recovered