CVE-2023-35931: Cleartext Storage of Sensitive Information in an Environment Variable (CWE-526)
value: Shescape is a simple shell escape library for JavaScript. An attacker may be able to get read-only access to environment variables. This bug has been patched in version 1.7.1.



CWE description: The product uses an environment variable to store unencrypted sensitive information.
CVSS v3 base score: 4.3
Published: 2023-06-23T20:15Z
Repository: https://github.com/ericcornelissen/shescape
Language: JavaScript
Files changed: CHANGELOG.md, src/win/cmd.js, test/fixtures/win.js, test/fuzz/_common.cjs, testing.js
Commit date: 2023-06-21 20:31:29 +0200
Commit message: Test coverage for environment variables (#982)

Add e2e test coverage for environment variables and add escaping of `%`
for CMD. ALso include `%` in test fixtures for all shells on Windows.

The new test cases for environment variables are also included in the
`shescape/testing` injectionStrings.
Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 150edc3..a1838fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,11 +5,13 @@ All notable changes to _Shescape_ will be documented in this file.
 The format is based on [Keep a Changelog], and this project adheres to [Semantic
 Versioning].
 
 ## [Unreleased]
 
+- Add `%` escaping for CMD. ([#982])
 - Correct documented behavior of quoting functions. ([#969])
+- Expand injection strings to cover environment variables. ([#982])
 
 ## [1.7.0] - 2023-06-12
 
 - Add flag protection option. ([#908])
 - Add `@since` information to API options. ([#909])
@@ -258,8 +260,9 @@ Versioning].
 [#902]: https://github.com/ericcornelissen/shescape/pull/902
 [#908]: https://github.com/ericcornelissen/shescape/pull/908
 [#909]: https://github.com/ericcornelissen/shescape/pull/909
 [#936]: https://github.com/ericcornelissen/shescape/pull/936
 [#969]: https://github.com/ericcornelissen/shescape/pull/969
+[#982]: https://github.com/ericcornelissen/shescape/pull/982
 [552e8ea]: https://github.com/ericcornelissen/shescape/commit/552e8eab56861720b1d4e5474fb65741643358f9
 [keep a changelog]: https://keepachangelog.com/en/1.0.0/
 [semantic versioning]: https://semver.org/spec/v2.0.0.html
diff --git a/src/win/cmd.js b/src/win/cmd.js
index 83b3c22..db5d1c4 100644
--- a/src/win/cmd.js
+++ b/src/win/cmd.js
@@ -12,11 +12,11 @@
 function escapeArgForInterpolation(arg) {
   return arg
     .replace(/[\0\u0008\u001B\u009B]/gu, "")
     .replace(/\r?\n|\r/gu, " ")
     .replace(/\^/gu, "^^")
-    .replace(/(["&<>|])/gu, "^$1");
+    .replace(/(["%&<>|])/gu, "^$1");
 }
 
 /**
  * Escape an argument for use in CMD when the argument is not being quoted (but
  * interpolation is inactive).
@@ -51,10 +51,11 @@ export function getEscapeFunction(options) {
  */
 function escapeArgForQuoted(arg) {
   return arg
     .replace(/[\0\u0008\u001B\u009B]/gu, "")
     .replace(/\r?\n|\r/gu, " ")
+    .replace(/%/gu, "^%")
     .replace(/"/gu, `""`);
 }
 
 /**
  * Quotes an argument for use in CMD.
diff --git a/test/fixtures/win.js b/test/fixtures/win.js
index 31d2693..216c954 100644
--- a/test/fixtures/win.js
+++ b/test/fixtures/win.js
@@ -852,10 +852,20 @@ export const escape = {
       {
         input: "a$b$c",
         expected: { interpolation: "a$b$c", noInterpolation: "a$b$c" },
       },
     ],
+    "percentage signs ('%')": [
+      {
+        input: "a%b",
+        expected: { interpolation: "a^%b", noInterpolation: "a%b" },
+      },
+      {
+        input: "a%b%c",
+        expected: { interpolation: "a^%b^%c", noInterpolation: "a%b%c" },
+      },
+    ],
     "ampersands ('&')": [
       {
         input: "a&b",
         expected: { interpolation: "a^&b", noInterpolation: "a&b" },
       },
@@ -2008,10 +2018,20 @@ export const escape = {
       {
         input: "a$b$c",
         expected: { interpolation: "a`$b`$c", noInterpolation: "a`$b`$c" },
       },
     ],
+    "percentage signs ('%')": [
+      {
+        input: "a%b",
+        expected: { interpolation: "a%b", noInterpolation: "a%b" },
+      },
+      {
+        input: "a%b%c",
+        expected: { interpolation: "a%b%c", noInterpolation: "a%b%c" },
+      },
+    ],
     "ampersands ('&')": [
       {
         input: "a&b",
         expected: { interpolation: "a`&b", noInterpolation: "a&b" },
       },
@@ -2947,10 +2967,20 @@ export const quote = {
       {
         input: "a$b$c",
         expected: '"a$b$c"',
       },
     ],
+    "percentage signs ('%')": [
+      {
+        input: "a%b",
+        expected: '"a^%b"',
+      },
+      {
+        input: "a%b%c",
+        expected: '"a^%b^%c"',
+      },
+    ],
     "left double quotation mark ('“')": [
       {
         input: "a“b",
         expected: '"a“b"',
       },
@@ -3145,10 +3175,20 @@ export const quote = {
       {
         input: "a$b$c",
         expected: '"a`$b`$c"',
       },
     ],
+    "percentage signs ('%')": [
+      {
+        input: "a%b",
+        expected: '"a%b"',
+      },
+      {
+        input: "a%b%c",
+        expected: '"a%b%c"',
+      },
+    ],
     "left double quotation mark ('“')": [
       {
         input: "a“b",
         expected: '"a““b"',
       },
diff --git a/test/fuzz/_common.cjs b/test/fuzz/_common.cjs
index 5af2085..f6ab24c 100644
--- a/test/fuzz/_common.cjs
+++ b/test/fuzz/_common.cjs
@@ -47,25 +47,31 @@ function isShellPowerShell(shell) {
 /**
  * Produces the expected echoed output.
  *
  * @param {object} args The function arguments.
  * @param {string} args.arg The input argument that was echoed.
+ * @param {boolean} args.quoted Was `arg` quoted prior to echoing.
  * @param {string} args.shell The shell used for echoing.
  * @param {boolean} normalizeWhitespace Whether whitespace should be normalized.
  * @returns {string} The expected echoed value.
  */
-function getExpectedOutput({ arg, shell }, normalizeWhitespace) {
+function getExpectedOutput({ arg, quoted, shell }, normalizeWhitespace) {
   // Remove control characters, like Shescape
   arg = arg.replace(/[\0\u0008\u001B\u009B]/gu, "");
 
   // Replace newline characters, like Shescape
   if (isShellCmd(shell) || isShellCsh(shell)) {
     arg = arg.replace(/\r?\n|\r/gu, " ");
   } else {
     arg = arg.replace(/\r(?!\n)/gu, "");
   }
 
+  // Adjust % for shell when quoted
+  if (isShellCmd(shell) && quoted) {
+    arg = arg.replace(/%/gu, "^%");
+  }
+
   if (normalizeWhitespace) {
     // Replace newline characters, like Shescape
     if (!isShellCmd(shell)) {
       arg = arg.replace(/\r?\n/gu, " ");
     }
diff --git a/testing.js b/testing.js
index 8cf6c5e..4269861 100644
--- a/testing.js
+++ b/testing.js
@@ -13,11 +13,19 @@ import { checkedToString, toArrayIfNecessary } from "./src/reflection.js";
  * for (const injectionString of injectionStrings) {
  *   const result = functionThatIsUsingShescape(injectionString);
  *   assert.equal(result, "no injection");
  * }
  */
-export const injectionStrings = ["\x00world", "&& ls", "'; ls #", '"; ls #'];
+export const injectionStrings = [
+  "\x00world",
+  "&& ls",
+  "'; ls #",
+  '"; ls #',
+  "$PATH",
+  "$Env:PATH",
+  "%PATH%",
+];
 
 /**
  * A test stub of shescape that has the same input-output profile as the real
  * shescape implementation.
  *

Vulnerable code:     .replace(/(["&<>|])/gu, "^$1");
function getExpectedOutput({ arg, shell }, normalizeWhitespace) {
export const injectionStrings = ["\x00world", "&& ls", "'; ls #", '"; ls #'];
Fixed code: - Add `%` escaping for CMD. ([#982])
- Expand injection strings to cover environment variables. ([#982])
[#982]: https://github.com/ericcornelissen/shescape/pull/982
    .replace(/(["%&<>|])/gu, "^$1");
    .replace(/%/gu, "^%")
    "percentage signs ('%')": [
      {
        input: "a%b",
        expected: { interpolation: "a^%b", noInterpolation: "a%b" },
      },
      {
        input: "a%b%c",
        expected: { interpolation: "a^%b^%c", noInterpolation: "a%b%c" },
      },
    ],
    "percentage signs ('%')": [
      {
        input: "a%b",
        expected: { interpolation: "a%b", noInterpolation: "a%b" },
      },
      {
        input: "a%b%c",
        expected: { interpolation: "a%b%c", noInterpolation: "a%b%c" },
      },
    ],
    "percentage signs ('%')": [
      {
        input: "a%b",
        expected: '"a^%b"',
      },
      {
        input: "a%b%c",
        expected: '"a^%b^%c"',
      },
    ],
    "percentage signs ('%')": [
      {
        input: "a%b",
        expected: '"a%b"',
      },
      {
        input: "a%b%c",
        expected: '"a%b%c"',
      },
    ],
 * @param {boolean} args.quoted Was `arg` quoted prior to echoing.
function getExpectedOutput({ arg, quoted, shell }, normalizeWhitespace) {
  // Adjust % for shell when quoted
  if (isShellCmd(shell) && quoted) {
    arg = arg.replace(/%/gu, "^%");
  }

export const injectionStrings = [
  "\x00world",
  "&& ls",
  "'; ls #",
  '"; ls #',
  "$PATH",
  "$Env:PATH",
  "%PATH%",
];