CVE-2022-25858: Inefficient Regular Expression Complexity (CWE-1333)
value: The package terser before 4.8.1, from 5.0.0 and before 5.14.2 are vulnerable to Regular Expression Denial of Service (ReDoS) due to insecure usage of regular expressions.
CWE description: The product uses a regular expression with an inefficient, possibly exponential worst-case computational complexity that consumes excessive CPU cycles.
CVSS v3 base score: 7.5
Published: 2022-07-15T20:15Z
Repository: https://github.com/terser/terser
Language: JavaScript
Files changed: lib/compress/evaluate.js, lib/compress/index.js, lib/utils/index.js, test/compress/regexp.js
Commit date: 2022-07-13 18:24:59 +0100
Commit message: fix potential regexp DDOS

Diff: diff --git a/lib/compress/evaluate.js b/lib/compress/evaluate.js
index 9cb432de..21d1c4a7 100644
--- a/lib/compress/evaluate.js
+++ b/lib/compress/evaluate.js
@@ -44,11 +44,12 @@
 import {
     HOP,
     makePredicate,
     return_this,
     string_template,
-    regexp_source_fix
+    regexp_source_fix,
+    regexp_is_safe,
 } from "../utils/index.js";
 import {
     AST_Array,
     AST_BigInt,
     AST_Binary,
@@ -127,11 +128,11 @@ def_eval(AST_Constant, function () {
 
 def_eval(AST_BigInt, return_this);
 
 def_eval(AST_RegExp, function (compressor) {
     let evaluated = compressor.evaluated_regexps.get(this.value);
-    if (evaluated === undefined) {
+    if (evaluated === undefined && regexp_is_safe(this.value.source)) {
         try {
             const { source, flags } = this.value;
             evaluated = new RegExp(source, flags);
         } catch (e) {
             evaluated = null;
diff --git a/lib/compress/index.js b/lib/compress/index.js
index 1f7529c4..651b4406 100644
--- a/lib/compress/index.js
+++ b/lib/compress/index.js
@@ -156,10 +156,11 @@ import {
     remove,
     return_false,
     return_true,
     regexp_source_fix,
     has_annotation,
+    regexp_is_safe,
 } from "../utils/index.js";
 import { first_in_statement } from "../utils/first_in_statement.js";
 import { equivalent_to } from "../equivalent-to.js";
 import {
     is_basic_identifier_string,
@@ -2138,10 +2139,11 @@ def_optimize(AST_Call, function(self, compressor) {
                 && self.args.every((arg) => {
                     var value = arg.evaluate(compressor);
                     params.push(value);
                     return arg !== value;
                 })
+                && regexp_is_safe(params[0])
             ) {
                 let [ source, flags ] = params;
                 source = regexp_source_fix(new RegExp(source).source);
                 const rx = make_node(AST_RegExp, self, {
                     value: { source, flags }
diff --git a/lib/utils/index.js b/lib/utils/index.js
index df864646..3a995c2e 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -247,11 +247,19 @@ function regexp_source_fix(source) {
             && (source[offset - 2] != "\\"
             || /(?:^|[^\\])(?:\\{2})*$/.test(source.slice(0, offset - 1)));
         return (escaped ? "" : "\\") + lineTerminatorEscape[match];
     });
 }
-const all_flags = "gimuy";
+
+// Subset of regexps that is not going to cause regexp based DDOS
+// https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
+const re_safe_regexp = /^[\\/|\0\s\w^$.[\]()]*$/;
+
+/** Check if the regexp is safe for Terser to create without risking a RegExp DOS */
+export const regexp_is_safe = (source) => re_safe_regexp.test(source);
+
+const all_flags = "dgimsuy";
 function sort_regexp_flags(flags) {
     const existing_flags = new Set(flags.split(""));
     let out = "";
     for (const flag of all_flags) {
         if (existing_flags.has(flag)) {
diff --git a/test/compress/regexp.js b/test/compress/regexp.js
index fe01fb8b..af31aaa8 100644
--- a/test/compress/regexp.js
+++ b/test/compress/regexp.js
@@ -28,28 +28,28 @@ regexp_2: {
     options = {
         evaluate: true,
         unsafe: true,
     }
     input: {
-        console.log(JSON.stringify("COMPASS? Overpass.".match(new RegExp("([Sap]+)", "ig"))));
+        console.log(JSON.stringify("COMPASS? Overpass.".match(new RegExp("(pass)", "ig"))));
     }
     expect: {
-        console.log(JSON.stringify("COMPASS? Overpass.".match(/([Sap]+)/gi)));
+        console.log(JSON.stringify("COMPASS? Overpass.".match(/(pass)/gi)));
     }
     expect_stdout: '["PASS","pass"]'
 }
 
 unsafe_slashes: {
     options = {
         defaults: true,
         unsafe: true
     }
     input: {
-        console.log(new RegExp("^https://"))
+        console.log(new RegExp("^https//"))
     }
     expect: {
-        console.log(/^https:\/\//)
+        console.log(/^https\/\//)
     }
 }
 unsafe_nul_byte: {
     options = {
         defaults: true,
@@ -73,5 +73,18 @@ inline_script: {
         /* </script> */
         /[</script>]/
     }
     expect_exact: '/* <\\/script> */\n/[<\\/script>]/;'
 }
+
+regexp_no_ddos: {
+    options = { unsafe: true, evaluate: true }
+    input: {
+        console.log(/(b+)b+/.test("bbb"))
+        console.log(RegExp("(b+)b+").test("bbb"))
+    }
+    expect: {
+        console.log(/(b+)b+/.test("bbb"))
+        console.log(RegExp("(b+)b+").test("bbb"))
+    }
+    expect_stdout: ["true", "true"]
+}

Vulnerable code:     regexp_source_fix
    if (evaluated === undefined) {
const all_flags = "gimuy";
        console.log(JSON.stringify("COMPASS? Overpass.".match(new RegExp("([Sap]+)", "ig"))));
        console.log(JSON.stringify("COMPASS? Overpass.".match(/([Sap]+)/gi)));
        console.log(new RegExp("^https://"))
        console.log(/^https:\/\//)
Fixed code:     regexp_source_fix,
    regexp_is_safe,
    if (evaluated === undefined && regexp_is_safe(this.value.source)) {
    regexp_is_safe,
                && regexp_is_safe(params[0])

// Subset of regexps that is not going to cause regexp based DDOS
// https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
const re_safe_regexp = /^[\\/|\0\s\w^$.[\]()]*$/;

/** Check if the regexp is safe for Terser to create without risking a RegExp DOS */
export const regexp_is_safe = (source) => re_safe_regexp.test(source);

const all_flags = "dgimsuy";
        console.log(JSON.stringify("COMPASS? Overpass.".match(new RegExp("(pass)", "ig"))));
        console.log(JSON.stringify("COMPASS? Overpass.".match(/(pass)/gi)));
        console.log(new RegExp("^https//"))
        console.log(/^https\/\//)

regexp_no_ddos: {
    options = { unsafe: true, evaluate: true }
    input: {
        console.log(/(b+)b+/.test("bbb"))
        console.log(RegExp("(b+)b+").test("bbb"))
    }
    expect: {
        console.log(/(b+)b+/.test("bbb"))
        console.log(RegExp("(b+)b+").test("bbb"))
    }
    expect_stdout: ["true", "true"]
}