VYPR
Moderate severityNVD Advisory· Published Feb 15, 2021· Updated Sep 16, 2024

Regular Expression Denial of Service (ReDoS)

CVE-2020-28500

Description

Lodash versions prior to 4.17.21 are vulnerable to Regular Expression Denial of Service (ReDoS) via the toNumber, trim and trimEnd functions.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Lodash before 4.17.21 is vulnerable to ReDoS via toNumber, trim, and trimEnd due to catastrophic backtracking in regex patterns.

Lodash versions prior to 4.17.21 contain a Regular Expression Denial of Service (ReDoS) vulnerability in the toNumber, trim, and trimEnd functions. The root cause is the use of regex patterns like /^\s+|\s+$/g and /\s+$/ that can cause catastrophic backtracking when processing large, specially crafted input strings [1][2]. This behavior is characteristic of ReDoS, where an attacker can craft input that forces the regex engine to take exponential time to evaluate, leading to excessive CPU consumption and denial of service [4].

The attack is exploitable without authentication in many scenarios, as these functions are commonly used to sanitize user-supplied input. An attacker can send a relatively short string (e.g., a long sequence of spaces followed by specific characters) to trigger the slowdown. The vulnerability is particularly severe because it affects a widely used utility library across many applications and platforms, including Node.js and browser environments [1][4].

The impact is a denial of service condition: the application becomes unresponsive or crashes as the CPU is consumed by the regex backtracking. Since Lodash is often used in server-side and client-side code, this can affect availability for legitimate users. The CVSS score is 7.5 (high), reflecting the low attack complexity and potential for significant impact [1].

The vulnerability is fixed in Lodash version 4.17.21. Users should upgrade to this version or later. The fix, introduced in pull request #5065 and commit c4847eb, replaces the problematic regex patterns with more efficient algorithms: baseTrim and trimmedEndIndex functions that avoid catastrophic backtracking by using character checks instead of complex regex [2][3]. No workarounds are recommended; upgrading is the definitive mitigation.

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
lodashnpm
>= 4.0.0, < 4.17.214.17.21
lodash-esnpm
>= 4.0.0, < 4.17.214.17.21
lodash.trimendnpm
>= 4.0.0, <= 4.5.1
lodash.trimnpm
>= 4.0.0, <= 4.5.1
lodash-railsRubyGems
>= 4.0.0, < 4.17.214.17.21

Affected products

6

Patches

1
c4847ebe7d14

Improve performance of `toNumber`, `trim` and `trimEnd` on large input strings

https://github.com/lodash/lodashMichał LipińskiJan 26, 2021via ghsa
2 files changed · +68 7
  • lodash.js+36 7 modified
    @@ -153,10 +153,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?/,
    @@ -1006,6 +1007,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.
        *
    @@ -1339,6 +1353,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.
        *
    @@ -12507,7 +12536,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)
    @@ -14998,7 +15027,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;
    @@ -15033,7 +15062,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;
    
  • test/test.js+32 0 modified
    @@ -23783,6 +23783,22 @@
     
           assert.deepEqual(actual, expected);
         });
    +
    +    QUnit.test('`_.`' + methodName + '` should prevent ReDoS', function(assert) {
    +      assert.expect(2);
    +
    +      var largeStrLen = 50000,
    +          largeStr = '1' + lodashStable.repeat(' ', largeStrLen) + '1',
    +          maxMs = 1000,
    +          startTime = lodashStable.now();
    +
    +      assert.deepEqual(_[methodName](largeStr), methodName == 'toNumber' ? NaN : 0);
    +
    +      var endTime = lodashStable.now(),
    +          timeSpent = endTime - startTime;
    +
    +      assert.ok(timeSpent < maxMs, 'operation took ' + timeSpent + 'ms');
    +    });
       });
     
       /*--------------------------------------------------------------------------*/
    @@ -24368,6 +24384,22 @@
           assert.strictEqual(func(string, ''), string);
         });
     
    +    QUnit.test('`_.`' + methodName + '` should prevent ReDoS', function(assert) {
    +      assert.expect(2);
    +
    +      var largeStrLen = 50000,
    +          largeStr = 'A' + lodashStable.repeat(' ', largeStrLen) + 'A',
    +          maxMs = 1000,
    +          startTime = lodashStable.now();
    +
    +      assert.strictEqual(_[methodName](largeStr), largeStr);
    +
    +      var endTime = lodashStable.now(),
    +          timeSpent = endTime - startTime;
    +
    +      assert.ok(timeSpent < maxMs, 'operation took ' + timeSpent + 'ms');
    +    });
    +
         QUnit.test('`_.' + methodName + '` should work as an iteratee for methods like `_.map`', function(assert) {
           assert.expect(1);
     
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

21

News mentions

0

No linked articles in our index yet.