Vulnerability: Regular Expression Denial of Service (ReDoS) in lodash All versions of package lodash prior to 4.17.21 are vulnerable to Regular Expression Denial of Service (ReDoS) via the toNumber, trim and trimEnd functions. Steps to reproduce (provided by reporter Liyuan Chen): var lo = require('lodash'); function build_blank (n) { var ret = "1" for (var i = 0; i < n; i++) { ret += " " } return ret + "1"; } var s = build_blank(50000) var time0 = Date.now(); lo.trim(s) var time_cost0 = Date.now() - time0; console.log("time_cost0: " + time_cost0) var time1 = Date.now(); lo.toNumber(s) var time_cost1 = Date.now() - time1; console.log("time_cost1: " + time_cost1) var time2 = Date.now(); lo.trimEnd(s) var time_cost2 = Date.now() - time2; console.log("time_cost2: " + time_cost2)
Vulnerability ID: GHSA-29mw-wpgm-hmr9
CWE: CWE-400
Severity score: 5.3
Project: https://github.com/lodash/lodash
Published: 2022-01-06
File: lodash.js
Commit message: perf: improve performance of `toNumber`, `trim` and `trimEnd` on large input strings
Commit author: Michał Lipiński

Vulnerable version (before fix):
   var reRegExpChar = /[\\^$.*+?()[\]{}|]/g,       reHasRegExpChar = RegExp(reRegExpChar.source);    /** Used to match leading and trailing whitespace. */   var reTrim = /^\s+|\s+$/g,       reTrimStart = /^\s+/,       reTrimEnd = /\s+$/;    /** Used to match wrap detail comments. */   var reWrapComment = /\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,     });   }    /**    * The base implementation of `_.unary` without support for storing metadata.    *       : asciiToArray(string);   }    /**    * Used by `_.unescape` to convert HTML entities to characters.    *       if (typeof value != 'string') {         return value === 0 ? value : +value;       }       value = value.replace(reTrim, '');       var isBinary = reIsBinary.test(value);       return (isBinary || reIsOctal.test(value))         ? freeParseInt(value.slice(2), isBinary ? 2 : 8)     function trim(string, chars, guard) {       string = toString(string);       if (string && (guard || chars === undefined)) {         return string.replace(reTrim, '');       }       if (!string || !(chars = baseToString(chars))) {         return string;     function trimEnd(string, chars, guard) {       string = toString(string);       if (string && (guard || chars === undefined)) {         return string.replace(reTrimEnd, '');       }       if (!string || !(chars = baseToString(chars))) {         return string;

Fixed version (after fix):
   var reRegExpChar = /[\\^$.*+?()[\]{}|]/g,       reHasRegExpChar = RegExp(reRegExpChar.source);    /** Used to match leading whitespace. */   var reTrimStart = /^\s+/;    /** Used to match a single whitespace character. */   var reWhitespace = /\s/;    /** Used to match wrap detail comments. */   var reWrapComment = /\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,     });   }    /**    * The base implementation of `_.trim`.    *    * @private    * @param {string} string The string to trim.    * @returns {string} Returns the trimmed string.    */   function baseTrim(string) {     return string       ? string.slice(0, trimmedEndIndex(string) + 1).replace(reTrimStart, '')       : string;   }    /**    * The base implementation of `_.unary` without support for storing metadata.    *       : asciiToArray(string);   }    /**    * Used by `_.trim` and `_.trimEnd` to get the index of the last non-whitespace    * character of `string`.    *    * @private    * @param {string} string The string to inspect.    * @returns {number} Returns the index of the last non-whitespace character.    */   function trimmedEndIndex(string) {     var index = string.length;      while (index-- && reWhitespace.test(string.charAt(index))) {}     return index;   }    /**    * Used by `_.unescape` to convert HTML entities to characters.    *       if (typeof value != 'string') {         return value === 0 ? value : +value;       }       value = baseTrim(value);       var isBinary = reIsBinary.test(value);       return (isBinary || reIsOctal.test(value))         ? freeParseInt(value.slice(2), isBinary ? 2 : 8)     function trim(string, chars, guard) {       string = toString(string);       if (string && (guard || chars === undefined)) {         return baseTrim(string);       }       if (!string || !(chars = baseToString(chars))) {         return string;     function trimEnd(string, chars, guard) {       string = toString(string);       if (string && (guard || chars === undefined)) {         return string.slice(0, trimmedEndIndex(string) + 1);       }       if (!string || !(chars = baseToString(chars))) {         return string;

Patch:
patch: @@ -152,10 +152,11 @@
   var reRegExpChar = /[\\^$.*+?()[\]{}|]/g,
       reHasRegExpChar = RegExp(reRegExpChar.source);
 
-  /** Used to match leading and trailing whitespace. */
-  var reTrim = /^\s+|\s+$/g,
-      reTrimStart = /^\s+/,
-      reTrimEnd = /\s+$/;
+  /** Used to match leading whitespace. */
+  var reTrimStart = /^\s+/;
+
+  /** Used to match a single whitespace character. */
+  var reWhitespace = /\s/;
 
   /** Used to match wrap detail comments. */
   var reWrapComment = /\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,
@@ -993,6 +994,19 @@
     });
   }
 
+  /**
+   * The base implementation of `_.trim`.
+   *
+   * @private
+   * @param {string} string The string to trim.
+   * @returns {string} Returns the trimmed string.
+   */
+  function baseTrim(string) {
+    return string
+      ? string.slice(0, trimmedEndIndex(string) + 1).replace(reTrimStart, '')
+      : string;
+  }
+
   /**
    * The base implementation of `_.unary` without support for storing metadata.
    *
@@ -1326,6 +1340,21 @@
       : asciiToArray(string);
   }
 
+  /**
+   * Used by `_.trim` and `_.trimEnd` to get the index of the last non-whitespace
+   * character of `string`.
+   *
+   * @private
+   * @param {string} string The string to inspect.
+   * @returns {number} Returns the index of the last non-whitespace character.
+   */
+  function trimmedEndIndex(string) {
+    var index = string.length;
+
+    while (index-- && reWhitespace.test(string.charAt(index))) {}
+    return index;
+  }
+
   /**
    * Used by `_.unescape` to convert HTML entities to characters.
    *
@@ -12494,7 +12523,7 @@
       if (typeof value != 'string') {
         return value === 0 ? value : +value;
       }
-      value = value.replace(reTrim, '');
+      value = baseTrim(value);
       var isBinary = reIsBinary.test(value);
       return (isBinary || reIsOctal.test(value))
         ? freeParseInt(value.slice(2), isBinary ? 2 : 8)
@@ -14979,7 +15008,7 @@
     function trim(string, chars, guard) {
       string = toString(string);
       if (string && (guard || chars === undefined)) {
-        return string.replace(reTrim, '');
+        return baseTrim(string);
       }
       if (!string || !(chars = baseToString(chars))) {
         return string;
@@ -15014,7 +15043,7 @@
     function trimEnd(string, chars, guard) {
       string = toString(string);
       if (string && (guard || chars === undefined)) {
-        return string.replace(reTrimEnd, '');
+        return string.slice(0, trimmedEndIndex(string) + 1);
       }
       if (!string || !(chars = baseToString(chars))) {
         return string;
path: lodash.js