How Lodash escapeRegExp Prevents Regex Injection
This article explains how the Lodash utility function
_.escapeRegExp neutralizes security vulnerabilities and
logic errors when creating dynamic regular expressions from untrusted
input. By converting functional regular expression syntax into harmless
literal characters, the method eliminates threats such as Regular
Expression Denial of Service (ReDoS) and unintended pattern
execution.
When applications dynamically construct regular expressions using
user input via the new RegExp(userInput) constructor, any
reserved regular expression characters included in that input are
interpreted by the JavaScript engine as functional operators. These
characters include ^, $, \,
., *, +, ?,
(, ), [, ],
{, }, and |. If an attacker
supplies inputs containing nested quantifiers—such as
(a+)+$—or broad match-all wildcards, they can manipulate
the query logic or trigger catastrophic backtracking, causing the
server's event loop to freeze in a ReDoS attack.
Lodash's _.escapeRegExp completely nullifies this threat
by intercepting the raw string before it reaches the RegExp
constructor. Internally, the function uses a targeted replacement
pattern that identifies every standard regular expression
metacharacter:
const reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
const reHasRegExpChar = RegExp(reRegExpChar.source);
function escapeRegExp(string) {
return string && reHasRegExpChar.test(string)
? string.replace(reRegExpChar, '\\$&')
: string || '';
}The function checks the string against reHasRegExpChar
for performance optimization. If special characters are detected, it
applies a global replacement where each matched metacharacter is
prepended with an escaping backslash (\\$&).
By prefixing each functional descriptor with a backslash, the JavaScript regular expression engine's lexical analyzer changes how it tokens the string:
- Quantifiers and Wildcards: Characters like
*,+, and.are converted to\*,\+, and\.. Instead of instructing the engine to evaluate dynamic lengths or match any character, the engine strictly evaluates them as literal asterisk, plus, or period glyphs. - Groups and Sets: Structural characters like
(),[], and{}become\(,\),\[,\],\{, and\}. This strips them of their capturing, non-capturing, range-defining, and counting capabilities, preventing attackers from injecting arbitrary sub-expressions or nested groups. - Anchors and Alternations: Characters like
^,$, and|are transformed to\^,\$, and\|, removing their ability to alter line boundaries or inject conditional branches.
Consider an unescaped search implementation:
// Vulnerable implementation
const search = "(a+)+";
const regex = new RegExp(search);
// Compiles to: /(a+)+/ (vulnerable to catastrophic backtracking)Applying _.escapeRegExp neutralizes the logic:
// Secure implementation with Lodash
const search = "(a+)+";
const safeSearch = _.escapeRegExp(search);
// Produces: "\\(a\\+\\)\\+"
const regex = new RegExp(safeSearch);
// Compiles to: /\(a\+\)\+/ (matches only the literal string "(a+)+")Because every metacharacter is reduced to its literal character equivalent, dynamically nested descriptors lose all operational syntax. The regular expression engine parses the sanitized string as a linear sequence of static characters, reducing the computational complexity of the evaluation from exponential time to deterministic, linear time (\(O(n)\)), completely mitigating regex injection.