
Product
Introducing Manifest Alerts
Socket now detects supply chain risks in project manifests, starting with missing lockfiles that can make dependency installs non-reproducible.
new-datadome-deobfuscator
Advanced tools
Babel AST-based deobfuscator for DataDome's WAFs (interstitial and captcha). Up to date and maintained for the latest versions. Clean files, extract dynamic challenges, pull out the embedded WASM payload.
Babel AST-based deobfuscator for DataDome's WAFs (interstitial and captcha). Up to date and maintained for the latest versions. Clean files, extract dynamic challenges, pull out the embedded WASM payload.

Before starting
Please kepe in mind that this Datadome daily update their challenge files (new compilation, new functions/identifiers names + dynamic challenges suffled). This repo has been done thanks to months of studying and analyzing their updates pattern and structures, with the goal of handling them correctly. Please leave a start and read my other works for supporting me,
Two scripts, one pipeline:
Both come out as clean, per-module files. Plus the post-processing extracts the per-session dynamic challenge expression and the embedded WASM payload + helpers that the bundle uses to compute its detection signature.
Pre vs post here:

So, DataDome is still using the same old obfuscation techniques, the only update they push sometime is another layer or operation just on top of the previous ones. Usually updating the deobfuscator means having just to handle a new layer or function concatenation.
The pipeline runs in eight phases:
\x48\x65\x6c\x6c\x6f → "Hello"
obj[['key']] → obj['key']
Detects which bundle shape lives at program.body[0] and splits the IIFE / function body into per-module ASTs. Captcha is a browserify-style !function(e,B,s){...}({...}), interstitial is a (function(){ var e={...}; ... })().
! function A(Q, t, C) {
// Modules handler function
}({
1: [function(A, Q, t) { /* Module with exports */ }, {}],
2: [function(A, Q, t) { /* Module with exports */ }, {}],
...
}, {}, [6]); // Entry point (Module 6 in this case)
After separation you get something like:
output/captcha/
captcha.js
vm-obf.js
bean.js
hash.js
helpers.js
main.js
mouseMaths.js
picasso.js
slidercaptcha.js
DataDome precomputes a 2D array whose cells are references to row-arrays, then uses <matrix>[x][y] everywhere as case values and table keys. The pass extracts the var e = (function(){...})() IIFE that builds it, runs that IIFE in a Node vm context, and rewrites every e[x][y] lookup to the small integer it actually represents. I call this TMatrix. I've already analyzed this logic in a previous article and repo.
Variables like let x = window.Number(-92) get executed and replaced inside the correct scope. We then have function calls like fn(numLit, numLit) (the actual string obfuscators) that now have their arguments displayed as literals, so we can fold them into their resulting strings.
T-matrix replacement, conditional and expression simplification, if-statement folding, opaque-predicate folding. Up to 10 rounds, until nothing changes.
Removal of "Mixed Boolean-Arithmetic" expressions like -3*(F&-881) + 1*~(F&o) + ... > -388. The pass samples each one across 20+ random integer assignments. If it's invariant, replace with the literal. If it's linear, fit a*x + b*y + c. (Random operations meant to "make the code hard to understand", we just sample our way through them.)
for (r = N; true;) { switch (r) { case X: ...; r = Y; continue; ... } } is flattened back into linear code. The first run handles straightforward cases. Then unused-variable removal strips the decoy for-inits DataDome pads in (for (var eA = 544; true;) where eA is never used and the real state lives in r), and the second run picks up whatever was hiding behind those decoys.
One file per separated module. Then extractDynamicChallenge, extractWasm, and extractWasmChallengeFields walk the cleaned ASTs and pull out the per-session signature expression, the embedded WASM bytes, and the ordered list of fields the WASM hashes. Those land on the result object directly (see Use it as a node module).
For the full per-phase walkthrough, the rationale behind each guard, and the historical fixes log, see LEARN.md.
npm install new-datadome-deobfuscator
Node 18 or newer. No native deps.
const fs = require('fs');
const { deobfuscate } = require('new-datadome-deobfuscator');
const source = fs.readFileSync('captcha.js', 'utf8');
const result = deobfuscate(source, { logLevel: 'INFO' });
console.log(result.bundleType); // 'captcha' | 'interstitial' | 'unknown'
console.log(result.moduleOrder); // ['captcha', 'vm-obf', 'bean', ...]
console.log(result.stats.reductionPercent); // '12.33%'
for (const [name, code] of Object.entries(result.modules)) {
fs.writeFileSync(`out/${name}.js`, code);
}
The result object:
| field | type | what it is |
|---|---|---|
bundleType | string | 'captcha', 'interstitial', or 'unknown' |
modules | object | { moduleName: deobfuscatedSource } |
moduleOrder | string[] | original emission order |
dynamic_challenge | string | null | the bit-twiddle expression DataDome computes per session for its request signature |
wasm | object | null | extracted WASM payload + helper metadata + hashed-fields list |
stats | object | { original, deobfuscated, reduction, reductionPercent } |
warnings | array | non-fatal issues |
errors | array | fatal issues with code and message |
report | object | structured report for downstream tooling |
Options:
| option | default | meaning |
|---|---|---|
logLevel | 'INFO' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'NONE' |
logger | (built-in) | bring your own Logger instance, overrides logLevel |
maxPasses | 10 | iterative-pass cap |
generatorOptions | (none) | forwarded to @babel/generator |
result.dynamic_challenge is a string containing the exact bit-twiddling expression DataDome runs per session over (build-sig, br_ow, br_oh, hardwareConcurrency) to produce its request signature. Decoder calls inside the expression are pre-resolved to literals, and the locally-named source arrays are renamed to consistent identifiers, so what you get is portable: drop it into your own solver and run it.
const { deobfuscate } = require('new-datadome-deobfuscator');
const fs = require('fs');
const source = fs.readFileSync('captcha.js', 'utf8');
const result = deobfuscate(source);
if (result.dynamic_challenge) {
// The expression as JS source. Plug into your solver / VM.
console.log(result.dynamic_challenge);
// Or persist for later replay
fs.writeFileSync('out/dynamic_challenge.js', result.dynamic_challenge);
} else {
console.warn('no dynamic challenge found, check warnings:', result.warnings);
}
Returns null when the extractor can't locate the call (rare, and usually means a new variant shape, check result.warnings).
result.wasm is the full WASM blob: the raw base64 bytes, the imports-provider source (so you can rebuild the import object), the list of window.* properties the provider touches, every external helper the provider transitively calls, and the ordered list of fields the compiled WASM actually hashes.
const { deobfuscate } = require('new-datadome-deobfuscator');
const fs = require('fs');
const source = fs.readFileSync('captcha.js', 'utf8');
const result = deobfuscate(source);
if (result.wasm) {
const { wasm, instance, providerName, windowAttributes, helpers, fields } = result.wasm;
// 1. Save the raw WASM bytes
fs.writeFileSync('out/datadome.wasm', Buffer.from(wasm, 'base64'));
// 2. Save the imports-provider so you can rebuild the import object
fs.writeFileSync('out/imports-provider.js', instance);
console.log('provider name:', providerName);
console.log('window attrs touched:', windowAttributes);
console.log('external helpers:', helpers.map(h => h.name));
console.log('hashed fields:', fields); // e.g. ['hardwareConcurrency', 'br_oh', 'maxTouchPoints', ...]
}
The wasm object shape:
| field | type | what it is |
|---|---|---|
wasm | string | raw WASM module, base64-encoded (starts with AGFzbQ) |
instance | string | source code of the imports-provider function |
providerName | string | name of the imports-provider in the original bundle |
windowAttributes | string[] | window.* properties the provider touches (DataView, Uint32Array, Error, …) |
helpers | array | { name, source } for every external helper the provider calls |
fields | string[] | ordered list of field names hashed by wasm_b (e.g. hardwareConcurrency, br_oh, maxTouchPoints) |
Returns null when the bundle has no embedded WASM or the extractor hits an unfamiliar shape.
NOTE The fields of the wasm_boring challenge changes daily, that's why we extract them alongisde the dynamic helpers, instance and windows attrs
# direct
npx datadome-deobfuscate input.js out/
# with a structured report on the side
npx datadome-deobfuscate input.js out/ --report out/report.json
Flags:
| flag | meaning |
|---|---|
--input <path> | alternative to the positional input argument |
--output <path> | alternative to the positional outputPrefix argument |
--report <path> | writes the full structured report (timestamp, per-module status, stats, errors, warnings, dynamic_challenge, wasm) |
--log-level <LEVEL> | DEBUG | INFO | WARN | ERROR | NONE, default INFO |
--no-delimiter | suppresses the ===DEOBFUSCATOR_RESULT=== line on stdout |
-h, --help | usage |
Exit codes:
| code | meaning |
|---|---|
| 0 | clean run, no errors (warnings tolerated) |
| 1 | fatal error: bad input, parse failure, or unexpected crash |
| 2 | finished, but at least one module recorded errors |
The line right after ===DEOBFUSCATOR_RESULT=== on stdout contains the same JSON the --report flag writes to disk. That's the contract for downstream tooling that wants to scrape the output without writing files:
report = json.loads(
out.split('===DEOBFUSCATOR_RESULT===', 1)[1].strip().splitlines()[0]
)
if report['status'] != 'success':
for e in report['errors']:
print(e['message'])
Suppress the delimiter with --no-delimiter if you don't want it in the stdout stream.
The post-processing extractors run automatically on every CLI invocation. The dynamic challenge expression and the full WASM blob land in the structured report on disk (and on the stdout delimiter line). No extra flags needed.
npx datadome-deobfuscate input/captcha.js out/captcha/ --report out/captcha/report.json
Then read the bits you care about:
# just the dynamic challenge expression
jq -r '.dynamic_challenge' out/captcha/report.json > out/dynamic_challenge.js
# the raw WASM bytes (base64-decoded)
jq -r '.wasm.wasm' out/captcha/report.json | base64 --decode > out/datadome.wasm
# the imports-provider source code
jq -r '.wasm.instance' out/captcha/report.json > out/imports-provider.js
# the ordered list of fields the WASM hashes
jq -r '.wasm.fields[]' out/captcha/report.json
Or if you're piping without writing to disk, scrape the stdout delimiter:
npx datadome-deobfuscate input/captcha.js out/captcha/ \
| sed -n '/===DEOBFUSCATOR_RESULT===/{n;p;}' \
| jq '{ dc: .dynamic_challenge, wasm_fields: .wasm.fields }'
Both dynamic_challenge and wasm always appear at the top level of the report payload, regardless of which module they were extracted from.
The repo doubles as a Vercel-deployable dashboard. Drop a captcha or interstitial file in the browser, watch the pipeline run, get the modules back as a zip.
Locally:
git clone https://github.com/glizzykingdreko/new-datadome-deobfuscator
cd new-datadome-deobfuscator
npm install
npm run dev
# → http://localhost:3000
Production:
npm i -g vercel
vercel deploy
| bundle | modules emitted |
|---|---|
| captcha | captcha, vm-obf, bean, hash, helpers, main, mouseMaths, picasso, slidercaptcha |
| interstitial | reloader, interstitial, obfuscate, helpers, vm-obf, localstorage, main |
Plus, when present:
dynamic_challenge: the bit-twiddle expression DataDome computes per session for its request signature.wasm: the embedded WASM module's bytes plus extracted helper metadata (provider name, window attributes touched, external helpers, hashed-fields list).new-datadome-deobfuscator/
lib/index.js Public Node.js API. deobfuscate(source, options).
bin/cli.js CLI entry point.
api/deobfuscate.js Vercel serverless function. Streaming + non-streaming.
api/deobfuscate-worker.js Worker thread that runs the pipeline and streams logs.
server.js Local dev server. Serves public/, routes /api/deobfuscate.
public/ Web UI. index.html, styles.css, app.js.
transformers/
index.js Pipeline runner.
captcha.js Bundle-shape detection + module separation.
preprocessing/index.js Phases 1 through 7 + recursive harvest.
transformations/ One file per pass: t-matrix, switch-case, opaque-predicates, ...
extractDynamicChallenge.js Pulls out the per-session bit-twiddle expression.
extractWasm.js Extracts WASM bytes + imports provider + helpers.
extractWasmChallengeFields.js Reads the ordered field list fed into wasm_b().
input/ Reference obfuscated bundles.
output/ Reference deobfuscated output.
LEARN.md Per-phase walkthrough + historical fixes log.
CLAUDE.md Agent instructions for this codebase.
tags.js, the third client-side script DataDome ships. Different bundle shape, different decoder location. Not in scope here.C(someVar) where someVar isn't a literal stays as-is.Other DataDome work I've published, all open-source:
Older repos, kept for reference but superseded by this one:
Other antibot vendors, same pattern:
Get in touch with some experts who truly understand the technology, don't think you want to have your project down each time Datadome pushes a change.
At TakionAPI we provide it. Be sure to check it out, start a free trial and then proceed checking out our documentation, one api call and Datadome is not a problem you need to worry about anymore.
Check our datadome-bypass-examples and be sure to start a free trial for testing them.
MIT. Author: glizzykingdreko.
This repo is the open-source companion to TakionAPI's DataDome solving API. If you don't want to wire up the full bypass yourself, TakionAPI ships a ready-to-plug solver.
FAQs
Babel AST-based deobfuscator for DataDome's WAFs (interstitial and captcha). Up to date and maintained for the latest versions. Clean files, extract dynamic challenges, pull out the embedded WASM payload.
The npm package new-datadome-deobfuscator receives a total of 3 weekly downloads. As such, new-datadome-deobfuscator popularity was classified as not popular.
We found that new-datadome-deobfuscator demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Product
Socket now detects supply chain risks in project manifests, starting with missing lockfiles that can make dependency installs non-reproducible.

Research
/Security News
The trojanized extensions use TinyGo-compiled WebAssembly and Solana transaction memos to resolve command-and-control infrastructure.

Security News
Anthropic says the directive cited national security concerns over a narrow jailbreak, but offered no specific technical details.