CVE-2024-36120 (TypeScript, published 2024-05-31T17:15Z)
Description: value: javascript-deobfuscator removes common JavaScript obfuscation techniques. In affected versions crafted payloads targeting expression simplification can lead to code execution. This issue has been patched in version 1.1.0. Users are advised to update. Users unable to upgrade should disable the expression simplification feature.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/ben-sb/javascript-deobfuscator
Files changed:
src/modifications/expressions/expressionSimplifier.ts
Commit message: fix: replaced expression simplification with approach from other deobfuscator


Vulnerable code:
                return this.simplifyBinaryExpression(expression);
                return this.simplifyUnaryExpression(expression);
    private simplifyBinaryExpression(expression: Shift.BinaryExpression): Shift.Expression {
        const leftValue = this.getExpressionValueAsString(left);
        const rightValue = this.getExpressionValueAsString(right);

        if (leftValue != null && rightValue != null) {
            const code = `${leftValue} ${expression.operator} ${rightValue}`;
            const simplified = this.evalCodeToExpression(code);
            return simplified != null ? simplified : expression;
        } else {
     * Attempts to simplify a unary expression node.
     * @param expression The unary expression node.
    private simplifyUnaryExpression(expression: Shift.UnaryExpression): Shift.Expression {
        expression.operand = this.simplifyExpression(expression.operand);
        const code = this.getExpressionValueAsString(expression);
        if (code != null) {
            const simplified = this.evalCodeToExpression(code);
            return simplified != null ? simplified : expression;
        } else {
            return expression;
     * Returns the value of a node as a string, null if not possible.
     * @param expression The expression node.
    private getExpressionValueAsString(expression: Shift.Expression): string | null {
            case 'LiteralStringExpression':
                const value = expression.value
                    .replace(/"/g, '\\"')
                    .replace(/\n/g, '\\n')
                    .replace(/\r/g, '\\r');
                return `"${value}"`;

                return expression.value.toString();

            case 'ArrayExpression':
                if (expression.elements.length == 0) {
                    return '[]';
                } else if (expression.elements.every(e => !e || e.type.startsWith('Literal'))) {
                    let content = '';
                    for (let i = 0; i < expression.elements.length; i++) {
                        if (expression.elements[i]) {
                            content += `${this.getExpressionValueAsString(
                                expression.elements[i] as Shift.Expression
                            )},`;
                        } else {
                            content += ',';
                        }
                    }
                    return `[${content.substring(0, content.length - 1)}]`;
                } else {
                    return null;
                }

            case 'ObjectExpression':
                if (expression.properties.length == 0) {
                    expression.properties;
                    return '[]';
                } else {
                    return null;
                }

                const operand = this.getExpressionValueAsString(expression.operand);
                return operand != null ? `${expression.operator} ${operand}` : null;

            default:
     * Evaluates a given piece of code and converts the result to an
     * expression node if possible.
     * @param code The code to be evaluated.
    private evalCodeToExpression(code: string): Shift.Expression | null {
        let value;
        try {
            value = eval(code);
        } catch (err) {
            return null;
        }

                return new Shift.LiteralStringExpression({
                    value: value
                });

                return new Shift.LiteralNumericExpression({
                    value: value
                });

                return new Shift.LiteralBooleanExpression({
                    value: value
                });

                return null;
    private isSimpleArray(array: Shift.ArrayExpression): boolean {
        return array.elements.every(e => !e || e.type.startsWith('Literal'));

Fixed code:
    private static readonly RESOLVABLE_UNARY_OPERATORS: Set<string> = new Set([
        '-',
        '+',
        '!',
        '~',
        'typeof',
        'void'
    ]);
    private static readonly RESOLVABLE_BINARY_OPERATORS: Set<string> = new Set([
        '==',
        '!=',
        '===',
        '!==',
        '<',
        '<=',
        '>',
        '>=',
        '<<',
        '>>',
        '>>>',
        '+',
        '-',
        '*',
        '/',
        '%',
        '**',
        '|',
        '^',
        '&'
    ]);
                return this.simplifyBinaryExpression(expression) || expression;
                return this.simplifyUnaryExpression(expression) || expression;
    /**
     * Attempts to simplify a unary expression node.
     * @param expression The unary expression node.
     */
    private simplifyUnaryExpression(expression: Shift.UnaryExpression): Shift.Expression | undefined {
        if (!ExpressionSimplifier.RESOLVABLE_UNARY_OPERATORS.has(expression.operator)) {
            return expression;
        } else if (expression.operator == '-' && expression.operand.type == 'LiteralNumericExpression') {
            return expression; // avoid trying to simplify negative numbers
        }

        const argument = this.simplifyExpression(expression.operand);

        if (this.isResolvableExpression(argument)) {
            const argumentValue = this.getResolvableExpressionValue(argument);
            const value = this.applyUnaryOperation(
                expression.operator as ResolvableUnaryOperator,
                argumentValue
            );
            return this.convertValueToExpression(value);
        } else {
            return expression;
        }
    }

    private simplifyBinaryExpression(expression: Shift.BinaryExpression): Shift.Expression | undefined {
        if (
            !expression.left.type.endsWith('Expression') ||
            !ExpressionSimplifier.RESOLVABLE_BINARY_OPERATORS.has(expression.operator)
        ) {
            return undefined;
        }

        if (this.isResolvableExpression(left) && this.isResolvableExpression(right)) {
            const leftValue = this.getResolvableExpressionValue(left);
            const rightValue = this.getResolvableExpressionValue(right);
            const value = this.applyBinaryOperation(
                expression.operator as ResolvableBinaryOperator,
                leftValue,
                rightValue
            );
            return this.convertValueToExpression(value);
        } else if (expression.operator == '-' && right.type == 'UnaryExpression' && right.operator == '-' && right.operand.type == 'LiteralNumericExpression') {
            // convert (- -a) to +a (as long as a is a number)
            expression.right = right.operand;
            expression.operator = '+';
        } else {
            return undefined;
     * Applies a unary operation.
     * @param operator The operator.
     * @param argument The argument value.
     * @returns The resultant value.
    private applyUnaryOperation(operator: ResolvableUnaryOperator, argument: any): any {
        switch (operator) {
            case '-':
                return -argument;
            case '+':
                return +argument;
            case '!':
                return !argument;
            case '~':
                return ~argument;
            case 'typeof':
                return typeof argument;
            case 'void':
                return void argument;
        }
    }
    /**
     * Applies a binary operation.
     * @param operator The resolvable binary operator.
     * @param left The value of the left expression.
     * @param right The value of the right expression.
     * @returns The resultant value.
     */
    private applyBinaryOperation(operator: ResolvableBinaryOperator, left: any, right: any): any {
        switch (operator) {
            case '==':
                return left == right;
            case '!=':
                return left != right;
            case '===':
                return left === right;
            case '!==':
                return left !== right;
            case '<':
                return left < right;
            case '<=':
                return left <= right;
            case '>':
                return left > right;
            case '>=':
                return left >= right;
            case '<<':
                return left << right;
            case '>>':
                return left >> right;
            case '>>>':
                return left >>> right;
            case '+':
                return left + right;
            case '-':
                return left - right;
            case '*':
                return left * right;
            case '/':
                return left / right;
            case '%':
                return left % right;
            case '**':
                return left ** right;
            case '|':
                return left | right;
            case '^':
                return left ^ right;
            case '&':
                return left & right;
     * Gets the real value from a resolvable expression.
     * @param expression The resolvable expression.
     * @returns The value.
    private getResolvableExpressionValue(expression: ResolvableExpression): any {
            case 'LiteralStringExpression':
                return expression.value;
                return -this.getResolvableExpressionValue(
                    expression.operand as Literal
                );
            case 'LiteralNullExpression':
            case 'IdentifierExpression':
                return undefined;
            case 'ArrayExpression':
                return [];
            case 'ObjectExpression':
                return {};
     * Attempts to convert a value of unknown type to an expression node.
     * @param value The value.
     * @returns The expression or undefined.
    private convertValueToExpression(value: any): Shift.Expression | undefined {
                return new Shift.LiteralStringExpression({ value });
                return value >= 0
                    ? new Shift.LiteralNumericExpression({ value })
                    : new Shift.UnaryExpression({ operator: '-', operand: new Shift.LiteralNumericExpression({ value: Math.abs(value) })});
                return new Shift.LiteralBooleanExpression({ value });
            case 'undefined':
                return new Shift.IdentifierExpression({ name: 'undefined' });
                return undefined;
    /**
     * Returns whether a node is a resolvable expression that can be
     * evaluated safely.
     * @param node The AST node.
     * @returns Whether.
     */
    private isResolvableExpression(node: Shift.Node): node is ResolvableExpression {
        return (
            this.isLiteral(node) ||
            (node.type == 'UnaryExpression' && node.operator == '-' && node.operand.type == 'LiteralNumericExpression') ||
            (node.type == 'IdentifierExpression' && node.name == 'undefined') ||
            (node.type == 'ArrayExpression' && node.elements.length == 0) ||
            (node.type == 'ObjectExpression' && node.properties.length == 0)
        );
    }

    /**
     * Returns whether a node is a literal.
     * @param node The AST node.
     * @returns Whether.
     */
    private isLiteral(node: Shift.Node): node is Literal {
        return node.type == 'LiteralNumericExpression' || node.type == 'LiteralStringExpression' || node.type == 'LiteralBooleanExpression' || node.type == 'LiteralNullExpression';

type Literal = Shift.LiteralNumericExpression | Shift.LiteralStringExpression | Shift.LiteralBooleanExpression | Shift.LiteralNullExpression;
type ResolvableExpression =
    | Literal
    | (Shift.UnaryExpression & { operator: '-'; argument: Literal })
    | (Shift.IdentifierExpression & { name: 'undefined' })
    | (Shift.ArrayExpression & { elements: [] })
    | (Shift.ObjectExpression & { properties: [] });

type ResolvableUnaryOperator = '-' | '+' | '!' | '~' | 'typeof' | 'void';

type ResolvableBinaryOperator =
    | '=='
    | '!='
    | '==='
    | '!=='
    | '<'
    | '<='
    | '>'
    | '>='
    | '<<'
    | '>>'
    | '>>>'
    | '+'
    | '-'
    | '*'
    | '/'
    | '%'
    | '**'
    | '|'
    | '^'
    | '&';