Skip to content

About

Open-source JavaScript reverse engineering toolkit for deobfuscation, AST analysis, string extraction, and web behavior discovery.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

JS Forensics: static analysis for minified and obfuscated JavaScript. Example: an escaped string, an atob call and a String.fromCharCode call rewritten as plain string literals.

JS Forensics

JS Forensics is a static-analysis toolkit for JavaScript that is minified, obfuscated, or simply unfamiliar. It parses a file, reports what is in it, and applies conservative, traceable transformations that undo common obfuscation patterns when it can prove the rewrite is safe.

It never executes the file it analyzes and never contacts a URL it finds.

Status: v0.2.0, alpha. Every transformation checks documented preconditions and skips, with a reason, anything it cannot prove. The output is checked for valid syntax, which is not proof that it behaves the same. See Known limitations before relying on a result.

What it does

  • Parses modern JavaScript with tree-sitter and reports syntax errors with line and column.
  • Resolves scopes: which declaration every name refers to, accounting for hoisting, block scoping, shadowing, and parameters.
  • Recognises string-array obfuscation (the provider, rotation, decoder, aliases, and wrapper functions that tools such as javascript-obfuscator generate), computes the rotation statically, and replaces each lookup with the string it returns.
  • Propagates constants, inlines constant object and array members and one-line proxy functions, folds constant expressions, simplifies constant conditions, removes branches that can never run, and restores code flattened into a while/switch dispatch loop.
  • Builds a function call graph, with the browser APIs, endpoints, and decoders used in each function, and lists calls it cannot resolve.
  • Detects twelve documented obfuscation indicators with evidence, extracts and classifies strings, lists browser API references and potential network endpoints.
  • Writes a terminal summary, JSON, a standalone HTML report, a unified diff, and Graphviz DOT for the call graph.
  • Scans a folder of .js/.mjs/.cjs files and maps relative imports between them.

Installation

Python 3.10 to 3.14 is supported.

$ pip install js-forensics
$ js-forensics --version
js-forensics 0.2.0

To install from source instead:

$ git clone https://github.com/stackadnan/js-forensics.git
$ cd js-forensics
$ python -m venv .venv && source .venv/bin/activate
$ pip install .

The Quick start examples use files in the repository's examples/ folder.

Quick start

The repository ships two obfuscated files to try things on: examples/sample.js (hand-written) and examples/obfuscated.js (output of javascript-obfuscator 4.2.2 with string arrays and control-flow flattening).

$ js-forensics analyze examples/sample.js
$ js-forensics analyze examples/sample.js --format json --output report.json
$ js-forensics strings examples/sample.js
$ js-forensics endpoints examples/sample.js
$ js-forensics deobfuscate examples/sample.js --output-dir output/
$ js-forensics report examples/sample.js --format html --output report.html
$ js-forensics diff examples/obfuscated.js
$ js-forensics callgraph examples/obfuscated.js --format dot --output calls.dot
$ js-forensics explain examples/obfuscated.js --line 70 --column 34
$ js-forensics passes

deobfuscate turns the 91-line examples/obfuscated.js (a 28-entry string array behind a checksum rotation, an alias-based decoder, a proxy object, and a flattened switch loop) into:

function greet(_0x128779) {
  console.log("Hello, " + _0x128779);
  fetch("https://api.example.com/v1/users", { method: "POST" });
  return _0x128779.length;
}
greet("world"), console.log("done", greet("again"));

and reports what each pass did:

Pass                      Changes  Skipped  Status
format-statements         45       0        changed in round 1, 2, 3
simplify-string-escapes   1        0        changed in round 1
resolve-string-arrays     20       0        changed in round 1, 2
decode-static-calls       0        0        no change
inline-object-literals    6        0        changed in round 1, 2
propagate-constants       2        0        changed in round 2
fold-constants            2        0        changed in round 1
simplify-conditionals     0        0        no change
remove-dead-branches      0        0        no change
unflatten-control-flow    2        0        changed in round 1
remove-unused-constants   5        0        changed in round 1, 2
simplify-property-access  15       0        changed in round 1
normalize-numbers         26       0        changed in round 1
4 round(s). Each accepted stage was parsed again without syntax errors. This checks syntax only; it
does not prove the program behaves the same.

Every change is recorded with its location and a reason in output/transformations.json, and the source after each changing stage is kept under output/stages/. The names stay machine-generated: JS Forensics does not guess identifier names.

examples/sample.js becomes:

function _0x2b7c(_0x1d){
  return "https://api.example.com/v1/session?token=" + _0x1d
}

fetch(_0x2b7c("abc"),{method:"POST",keepalive:true});
setTimeout(function(){
  console.log("done",60000)
},1000);

explain shows everything known about a position, including what a string-array call returns:

examples/obfuscated.js, line 70, column 34
          }, _0x214be7 = _0x7dd790[_0x5e13b6(0x1b6)][_0x5e13b6(0x1b9)]('|'), _0x25262a = 0x0;
Inside: greet@58:1
Kind               From   To     Finding
call               70:34  70:50  call of _0x1269@2:1 (resolved)
string-array-call  70:34  70:50  _0x2c89 returns 'QKgfd'

Commands

Command Purpose
analyze FILE Summary: statistics, indicators, string arrays, functions, strings, APIs, endpoints. --format text|json.
strings FILE String and template literals with categories. --category filters.
endpoints FILE Potential network endpoints, domains, and request calls whose URL is dynamic.
deobfuscate FILE Run the passes and write the result to --output-dir (default js-forensics-output/).
diff FILE Unified diff between the file and its transformed version. Writes nothing unless --output is given.
passes List the passes, their default order, and their preconditions.
callgraph FILE Functions, calls between them, and what each uses. --format text|json|dot.
explain FILE --line N [--column N] Every finding at a source position.
project DIRECTORY Analyze every .js/.mjs/.cjs file in a folder and map relative imports.
report FILE Standalone report. --format html|json; --transformations adds the pipeline history.

deobfuscate and diff take --passes a,b,c (run exactly these, in this order), --skip a,b, and --max-rounds N. Analysis commands take --output/-o, --tolerant (analyze despite syntax errors and mark results as partial), --no-redact, and --max-bytes (default 2 MiB).

Exit codes: 0 success, 2 invalid command line, 3 input missing, unreadable, too large, or not UTF-8, 4 JavaScript syntax error, 5 output could not be written. 1 is an unexpected internal error.

docs/usage.md is the full reference. docs/transformations.md documents every pass: what it recognises, its preconditions, and what it leaves alone. docs/output-schema.md describes the JSON.

Transformations

Passes run in a fixed default order, and the sequence repeats until a round changes nothing (at most six rounds), because one pass often enables another: a resolved string array exposes constants to propagate, which exposes a dispatch order string to unflatten.

Pass Undoes
format-statements Minified layout (whitespace between statements only).
simplify-string-escapes "\x68\x69" → "hi".
resolve-string-arrays String-array lookups such as _0x1269(0x1a8).
decode-static-calls atob, String.fromCharCode, String.fromCodePoint, decodeURIComponent, decodeURI, unescape on constants.
inline-object-literals O.key and O.add(a, b) on objects and arrays that never change.
propagate-constants Variables that only ever hold one literal.
fold-constants 0x3e8 * 0x3c, !![], typeof "s", x + "a" + "b".
simplify-conditionals true ? a : b, 1 && x, 0 || x.
remove-dead-branches if (false) {...} and statements after return.
unflatten-control-flow while (true) switch (order[i++]) dispatch loops.
remove-unused-constants Declarations nothing reads after the above.
simplify-property-access o["name"] → o.name.
normalize-numbers 0x1a → 26.

What is supported, what is heuristic, and what stays unresolved:

  • Proven rewrites. Every pass above changes code only when its documented preconditions hold, using scope analysis to rule out shadowing and reassignment. If a precondition fails, the code is left alone and the reason is recorded.
  • Recognised shapes. String arrays are resolved for the provider, decoder, alias, wrapper, and rotation shapes listed in docs/transformations.md, which cover javascript-obfuscator 4.x and 5.x output without string encoding. Dispatch loops are unflattened only in the exact shape described there.
  • Heuristic, not transformed. The obfuscation indicators and score, string categories, credential redaction, and browser API confidence levels.
  • Unresolved. String arrays with base64 or RC4 encoding, values computed at runtime, code inside eval/Function, and any shape not listed are reported but not changed.

Transformations replace exact source ranges; untouched code stays byte-for-byte as written. After every stage the result is parsed again; a stage whose output does not parse, or that grows the file more than fourfold, is discarded and reported as reverted.

During development, the default pipeline was run on javascript-obfuscator 4.2.2 and 5.9.0 output of the test suite's synthetic programs across several option presets and seeds (162 runs). Each original and result was executed in an isolated Node.js context, using only those trusted test programs, and logged the same calls. The obfuscator samples in tests/fixtures/obfuscator/ are checked the same way in the test suite when Node.js is installed. This is evidence for the tested shapes, not a guarantee for other input.

Supported analysis techniques

Area How it works
Statistics Counts of syntax-node types.
Scope analysis Lexical scopes, hoisting, and every name resolved to its declaration. Files using with or opaque direct eval are reported as unreliable and not transformed.
Obfuscation indicators Twelve structural rules with fixed thresholds and weights. The score is the capped sum of the weights that triggered.
String classification Pattern heuristics. Ambiguous strings stay other.
Static evaluation Reimplementations of string escapes, number formatting, parseInt, atob, URI decoding, and a subset of operators, applied only to constants.
Call graph Calls resolved through scopes; everything else is labelled external or unresolved.
Browser APIs A catalogue of global names and member chains, plus distinctive method names at lower confidence.
Endpoints Arguments of fetch, XMLHttpRequest.open, WebSocket, EventSource, navigator.sendBeacon, axios, and jQuery, plus URL-like strings and constant expressions.
Project scan Per-file analysis plus relative import, export … from, import(), and require() resolution within the folder.

Security boundaries

Every input is treated as hostile.

  • The analyzed file is read and parsed. It is never evaluated, imported, or passed to a JavaScript runtime. Code strings passed to Function or eval are parsed to check whether they could reassign variables; they are never run.
  • No network access happens. URLs are extracted as text. The test suite fails if a socket is opened.
  • The original file is never modified. Outputs go to a separate path and a write that would land on the input is refused.
  • project never follows symbolic links and never reads outside the given folder.
  • Input size is limited (2 MiB per file by default), nesting depth is bounded in the evaluators, the pipeline stops after six rounds, and a stage that would grow the file more than fourfold is reverted.
  • HTML reports contain no script, carry a default-src 'none' Content-Security-Policy, and escape all text taken from the file, including strings that only appear after deobfuscation. URLs are shown as text, not links.
  • Terminal and HTML output escape control characters, so a file cannot inject ANSI sequences or bidirectional overrides into what you read.
  • Likely credentials are masked in reports (pattern matching; it will miss some). --no-redact turns it off. The deobfuscated JavaScript itself is never redacted.
  • A finding describes source text. A listed endpoint or API reference does not mean the code runs.

Known limitations

  • Values are only resolved when they are constant: literals, variables that only ever hold one literal, constant object members, and recognised string-array calls. Strings assembled from runtime data are not recovered.
  • String arrays with base64 or RC4 encoding are reported as unsupported. Other obfuscators' string tables are resolved only if they happen to match the documented shapes.
  • Control-flow unflattening handles only the while/switch dispatch shape. Other flattening schemes, opaque predicates that depend on runtime values, and dead-code injection that does not reduce to a constant condition are left as they are.
  • No renaming of identifiers.
  • Program-level declarations are only removed when their names look machine-generated (_0x…), since other scripts on a page can use a script's globals. The analysis assumes that no other script reassigns this file's global variables.
  • Built-ins (String, atob, parseInt, String.prototype.split) are assumed not to be replaced at runtime.
  • The call graph does not resolve method calls, callbacks, or calls through object properties and parameters.
  • project resolves relative imports only; packages, path aliases, and TypeScript are not supported.
  • The parser is tree-sitter's JavaScript grammar. It also accepts JSX and does not report every early error an engine would. TypeScript is not supported.
  • Only UTF-8 input is accepted.
  • Memory and time grow with input size. On the development machine, a full analysis with transformations of 1 MB of dense minified source took about 23 seconds and peaked near 700 MB, which is why the default limit is 2 MiB.

Contributing

See CONTRIBUTING.md for setup, the checks to run, and the rules for adding detectors and transformations. docs/architecture.md explains how the code is laid out.

License

MIT. See LICENSE.

Security problems should be reported privately; see SECURITY.md.

About

Open-source JavaScript reverse engineering toolkit for deobfuscation, AST analysis, string extraction, and web behavior discovery.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages