js-deobfuscator 2.0.0

Universal JavaScript deobfuscator built on OXC
Documentation
//! Safety checker — determines which expressions are safe to send to Node.js.
//!
//! Only deterministic, side-effect-free expressions with known-safe globals.

use oxc::ast::ast::Expression;

/// Safe global objects (read-only, deterministic).
/// Note: Date is NOT included because Date.now() is non-deterministic.
const SAFE_OBJECTS: &[&str] = &["Math", "JSON", "Number", "String", "Array", "Object"];

/// Safe global functions.
const SAFE_FUNCTIONS: &[&str] = &[
    "parseInt", "parseFloat", "isNaN", "isFinite",
    "encodeURI", "decodeURI", "encodeURIComponent", "decodeURIComponent",
    "atob", "btoa", "escape", "unescape",
    "Number", "String", "Boolean",
];

/// Unsafe globals — never send to Node.js.
const UNSAFE_GLOBALS: &[&str] = &[
    "eval", "Function", "require", "import", "process",
    "console", "fetch", "XMLHttpRequest", "WebSocket",
    "setTimeout", "setInterval", "setImmediate",
    "global", "globalThis", "window", "document", "navigator",
    "fs", "child_process", "net", "http", "https",
];

/// Check if an expression is safe to evaluate in Node.js.
pub fn is_safe_expr(expr: &Expression) -> bool {
    match expr {
        // Literals are always safe
        Expression::NumericLiteral(_)
        | Expression::StringLiteral(_)
        | Expression::BooleanLiteral(_)
        | Expression::NullLiteral(_) => true,

        // Known-safe identifiers
        Expression::Identifier(id) => {
            let name = id.name.as_str();
            !UNSAFE_GLOBALS.contains(&name)
        }

        // Operators with safe operands
        Expression::BinaryExpression(b) => is_safe_expr(&b.left) && is_safe_expr(&b.right),
        Expression::UnaryExpression(u) => is_safe_expr(&u.argument),
        Expression::LogicalExpression(l) => is_safe_expr(&l.left) && is_safe_expr(&l.right),
        Expression::ConditionalExpression(c) => {
            is_safe_expr(&c.test) && is_safe_expr(&c.consequent) && is_safe_expr(&c.alternate)
        }

        // Call expressions — check callee is safe
        Expression::CallExpression(call) => {
            // All args must be safe
            if !call.arguments.iter().all(|a| a.as_expression().is_some_and(is_safe_expr)) {
                return false;
            }
            match &call.callee {
                // Global function: parseInt(), atob(), etc.
                Expression::Identifier(id) => SAFE_FUNCTIONS.contains(&id.name.as_str()),
                // Method call: Math.floor(), "abc".toUpperCase(), etc.
                Expression::StaticMemberExpression(m) => {
                    matches!(&m.object, Expression::Identifier(id) if SAFE_OBJECTS.contains(&id.name.as_str()))
                        || matches!(&m.object, Expression::StringLiteral(_))
                }
                _ => false,
            }
        }

        // Member access on safe objects
        Expression::StaticMemberExpression(m) => is_safe_expr(&m.object),
        Expression::ComputedMemberExpression(m) => is_safe_expr(&m.object) && is_safe_expr(&m.expression),

        // Parenthesized
        Expression::ParenthesizedExpression(p) => is_safe_expr(&p.expression),

        // Array/object literals with safe elements
        Expression::ArrayExpression(arr) => arr.elements.iter().all(|el| {
            el.as_expression().is_some_and(is_safe_expr)
        }),

        // Template literals
        Expression::TemplateLiteral(t) => t.expressions.iter().all(is_safe_expr),

        // Everything else: unsafe
        _ => false,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use oxc::allocator::Allocator;
    use oxc::parser::Parser;
    use oxc::span::SourceType;

    fn parse_expr<'a>(alloc: &'a Allocator, src: &'a str) -> Expression<'a> {
        Parser::new(alloc, src, SourceType::mjs()).parse_expression().unwrap()
    }

    #[test]
    fn test_safe_literals() {
        let a = Allocator::default();
        assert!(is_safe_expr(&parse_expr(&a, "42")));
        assert!(is_safe_expr(&parse_expr(&a, "\"hello\"")));
    }

    #[test]
    fn test_safe_math() {
        let a = Allocator::default();
        assert!(is_safe_expr(&parse_expr(&a, "Math.floor(1.7)")));
        assert!(is_safe_expr(&parse_expr(&a, "parseInt(\"42\")")));
    }

    #[test]
    fn test_unsafe_eval() {
        let a = Allocator::default();
        assert!(!is_safe_expr(&parse_expr(&a, "eval(\"code\")")));
    }

    #[test]
    fn test_unsafe_process() {
        let a = Allocator::default();
        assert!(!is_safe_expr(&parse_expr(&a, "process.exit()")));
    }

    #[test]
    fn test_unsafe_require() {
        let a = Allocator::default();
        assert!(!is_safe_expr(&parse_expr(&a, "require(\"fs\")")));
    }
}