CVE-2022-25918: Inefficient Regular Expression Complexity (CWE-1333)
value: The package shescape from 1.5.10 and before 1.6.1 are vulnerable to Regular Expression Denial of Service (ReDoS) via the escape function in index.js, due to the usage of insecure regex in the escapeArgBash function.
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-10-27T10:15Z
Repository: https://github.com/ericcornelissen/shescape
Language: JavaScript
Files changed: SECURITY.md, src/unix.js, test/fixtures/unix.js, test/unit/unix/escape.test.js
Commit date: 2022-10-25 19:49:23 +0200
Commit message: Merge pull request from GHSA-cr84-xvw4-qx3c

* Test polynomial backtracking ReDoS attack string on Bash

Create a unit test for a (new) ReDoS string on bash. This test should be
failing in this commit.

* Fix ReDoS (as seen in ReDoS test 1) for bash

Update bash escaping to pessimistically escape opening curly braces,
similar to Zsh escaping, to fix the ReDoS due to the complex regular
expression for opening curly braces.

Update tests accordingly.

* Test against old ReDoS values and all shells

Refactor the ReDoS test such that 1) it's tested against all (supported)
shells, and 2) extensible to test against multiple potential ReDoS
strings. Regarding the latter, the scope has been updated to include
known ReDoS strings related to CVE-2022-36064.

The `redos` value is intentionally a function as this avoids building
the string unnecessarily if the file is imported but the `redos` value
isn't used.

* Update SECURITY.md

Add latest advisory and credit its finder
Diff: diff --git a/SECURITY.md b/SECURITY.md
index 11169b8..28cd4d1 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -25,20 +25,23 @@ Request in the public repository.
 If you found a security bug in an unsupported version of the project, please
 report this publicly. For example, as a regular issue in the public repository.
 
 ## Advisories
 
-| ID               | Date       | Affected versions | Patched versions |
-| :--------------- | :--------- | :---------------- | :--------------- |
-| `CVE-2021-21384` | 2021-03-19 | `<1.1.3`          | `1.1.3`          |
-| `CVE-2022-24725` | 2022-03-03 | `>=1.4.0 <1.5.1`  | `1.5.1`          |
-| `CVE-2022-31179` | 2022-07-26 | `<1.5.8`          | `1.5.8`          |
-| `CVE-2022-31180` | 2022-07-26 | `>=1.4.0 <1.5.8`  | `1.5.8`          |
-| `CVE-2022-36064` | 2022-08-29 | `>=1.5.1 <1.5.10` | `1.5.10`         |
+| ID                    | Date       | Affected versions | Patched versions |
+| :-------------------- | :--------- | :---------------- | :--------------- |
+| `CVE-2021-21384`      | 2021-03-19 | `<1.1.3`          | `1.1.3`          |
+| `CVE-2022-24725`      | 2022-03-03 | `>=1.4.0 <1.5.1`  | `1.5.1`          |
+| `CVE-2022-31179`      | 2022-07-26 | `<1.5.8`          | `1.5.8`          |
+| `CVE-2022-31180`      | 2022-07-26 | `>=1.4.0 <1.5.8`  | `1.5.8`          |
+| `CVE-2022-36064`      | 2022-08-29 | `>=1.5.1 <1.5.10` | `1.5.10`         |
+| `GHSA-cr84-xvw4-qx3c` | 2022-10-25 | `>=1.5.10 <1.6.1` | `1.6.1`          |
 
 ## Acknowledgments
 
 We would like to publicly thank the following reporters:
 
-- _None yet_
+- Elliot Ward ([@mowzk]) from [Snyk]
 
+[@mowzk]: https://github.com/mowzk
 [security@ericcornelissen.dev]: mailto:security@ericcornelissen.dev?subject=SECURITY%20%28shescape%29
+[snyk]: https://snyk.io/
diff --git a/src/unix.js b/src/unix.js
index 5f2d4dc..d3bb299 100644
--- a/src/unix.js
+++ b/src/unix.js
@@ -46,14 +46,11 @@ function escapeArgBash(arg, interpolation, quoted) {
   if (interpolation) {
     result = result
       .replace(/\\/gu, "\\\\")
       .replace(/\n/gu, " ")
       .replace(/(^|\s)([#~])/gu, "$1\\$2")
-      .replace(/(["$&'()*;<>?`|])/gu, "\\$1")
-      .replace(/(?<!\{)\{+(?=(?:[^{][^,.]*)?[,.][^}]*\})/gu, (curlyBraces) =>
-        curlyBraces.replace(/\{/gu, "\\{")
-      )
+      .replace(/(["$&'()*;<>?`{|])/gu, "\\$1")
       .replace(/(?<=[:=])(~)(?=[\s+\-/0:=]|$)/gu, "\\$1");
   } else if (quoted) {
     result = result.replace(/'/gu, `'\\''`);
   }
 
diff --git a/test/fixtures/unix.js b/test/fixtures/unix.js
index 4356664..658c7a1 100644
--- a/test/fixtures/unix.js
+++ b/test/fixtures/unix.js
@@ -1114,27 +1114,27 @@ export const escape = {
       },
     ],
     "curly brackets ('{', '}')": [
       {
         input: "a{b",
-        expected: { interpolation: "a{b", noInterpolation: "a{b" },
+        expected: { interpolation: "a\\{b", noInterpolation: "a{b" },
       },
       {
         input: "a}b",
         expected: { interpolation: "a}b", noInterpolation: "a}b" },
       },
       {
         input: "a{b{c",
-        expected: { interpolation: "a{b{c", noInterpolation: "a{b{c" },
+        expected: { interpolation: "a\\{b\\{c", noInterpolation: "a{b{c" },
       },
       {
         input: "a}b}c",
         expected: { interpolation: "a}b}c", noInterpolation: "a}b}c" },
       },
       {
         input: "a{b}c",
-        expected: { interpolation: "a{b}c", noInterpolation: "a{b}c" },
+        expected: { interpolation: "a\\{b}c", noInterpolation: "a{b}c" },
       },
       {
         input: "a{b,c}d",
         expected: { interpolation: "a\\{b,c}d", noInterpolation: "a{b,c}d" },
       },
@@ -3599,5 +3599,15 @@ export const quote = {
         expected: { escaped: "'a'", notEscaped: "'a'" },
       },
     ],
   },
 };
+
+export const redos = () => [
+  // CVE-2022-36064
+  `foo${"{".repeat(150_000)}bar`,
+  `=${":".repeat(150_000)}foobar`,
+  `{${",".repeat(150_000)}`,
+
+  // No identifier (yet)
+  "{,".repeat(150_000),
+];
diff --git a/test/unit/unix/escape.test.js b/test/unit/unix/escape.test.js
index ea3e832..00bb60e 100644
--- a/test/unit/unix/escape.test.js
+++ b/test/unit/unix/escape.test.js
@@ -44,6 +44,26 @@ Object.entries(fixtures.escape).forEach(([shellName, scenarios]) => {
       shellName,
     });
   });
 });
 
+fixtures.redos().forEach((s, i) => {
+  test(`bash, ReDoS #${i}`, (t) => {
+    const escape = unix.getEscapeFunction("bash");
+    escape(s, true, false);
+    t.pass();
+  });
+
+  test(`dash, ReDoS #${i}`, (t) => {
+    const escape = unix.getEscapeFunction("dash");
+    escape(s, true, false);
+    t.pass();
+  });
+
+  test(`zsh, ReDoS #${i}`, (t) => {
+    const escape = unix.getEscapeFunction("zsh");
+    escape(s, true, false);
+    t.pass();
+  });
+});
+
 test(macros.unsupportedShell, { fn: unix.getEscapeFunction });

Vulnerable code: | ID               | Date       | Affected versions | Patched versions |
| :--------------- | :--------- | :---------------- | :--------------- |
| `CVE-2021-21384` | 2021-03-19 | `<1.1.3`          | `1.1.3`          |
| `CVE-2022-24725` | 2022-03-03 | `>=1.4.0 <1.5.1`  | `1.5.1`          |
| `CVE-2022-31179` | 2022-07-26 | `<1.5.8`          | `1.5.8`          |
| `CVE-2022-31180` | 2022-07-26 | `>=1.4.0 <1.5.8`  | `1.5.8`          |
| `CVE-2022-36064` | 2022-08-29 | `>=1.5.1 <1.5.10` | `1.5.10`         |
- _None yet_
      .replace(/(["$&'()*;<>?`|])/gu, "\\$1")
      .replace(/(?<!\{)\{+(?=(?:[^{][^,.]*)?[,.][^}]*\})/gu, (curlyBraces) =>
        curlyBraces.replace(/\{/gu, "\\{")
      )
        expected: { interpolation: "a{b", noInterpolation: "a{b" },
        expected: { interpolation: "a{b{c", noInterpolation: "a{b{c" },
        expected: { interpolation: "a{b}c", noInterpolation: "a{b}c" },
Fixed code: | ID                    | Date       | Affected versions | Patched versions |
| :-------------------- | :--------- | :---------------- | :--------------- |
| `CVE-2021-21384`      | 2021-03-19 | `<1.1.3`          | `1.1.3`          |
| `CVE-2022-24725`      | 2022-03-03 | `>=1.4.0 <1.5.1`  | `1.5.1`          |
| `CVE-2022-31179`      | 2022-07-26 | `<1.5.8`          | `1.5.8`          |
| `CVE-2022-31180`      | 2022-07-26 | `>=1.4.0 <1.5.8`  | `1.5.8`          |
| `CVE-2022-36064`      | 2022-08-29 | `>=1.5.1 <1.5.10` | `1.5.10`         |
| `GHSA-cr84-xvw4-qx3c` | 2022-10-25 | `>=1.5.10 <1.6.1` | `1.6.1`          |
- Elliot Ward ([@mowzk]) from [Snyk]
[@mowzk]: https://github.com/mowzk
[snyk]: https://snyk.io/
      .replace(/(["$&'()*;<>?`{|])/gu, "\\$1")
        expected: { interpolation: "a\\{b", noInterpolation: "a{b" },
        expected: { interpolation: "a\\{b\\{c", noInterpolation: "a{b{c" },
        expected: { interpolation: "a\\{b}c", noInterpolation: "a{b}c" },

export const redos = () => [
  // CVE-2022-36064
  `foo${"{".repeat(150_000)}bar`,
  `=${":".repeat(150_000)}foobar`,
  `{${",".repeat(150_000)}`,

  // No identifier (yet)
  "{,".repeat(150_000),
];
fixtures.redos().forEach((s, i) => {
  test(`bash, ReDoS #${i}`, (t) => {
    const escape = unix.getEscapeFunction("bash");
    escape(s, true, false);
    t.pass();
  });

  test(`dash, ReDoS #${i}`, (t) => {
    const escape = unix.getEscapeFunction("dash");
    escape(s, true, false);
    t.pass();
  });

  test(`zsh, ReDoS #${i}`, (t) => {
    const escape = unix.getEscapeFunction("zsh");
    escape(s, true, false);
    t.pass();
  });
});