Securing Lodash _.template Against Code Injection
This article examines how Lodash’s _.template function
executes dynamic templates, analyzes the risks associated with dynamic
code injection, and details the specific mechanisms used to constrain
and secure execution contexts. By understanding Lodash's internal
compilation to the JavaScript Function constructor,
developers can implement strict sandboxing, delimiter constraints, and
variable scoping to neutralize malicious execution dynamically.
The Mechanics of
_.template
Lodash’s _.template utility converts a template string
into an executable JavaScript function. Internally, Lodash parses the
string using regular expressions that identify three distinct
syntactical delimiters:
- Interpolate (
<%= %>): Evaluates expressions and outputs raw values. - Escape (
<%- %>): Evaluates expressions and outputs HTML-escaped values. - Evaluate (
<% %>): Executes arbitrary JavaScript logic, such as loops or conditionals.
During compilation, Lodash builds a dynamic string representing the
function body and instantiates it via new Function(...).
Because this dynamically generated string is evaluated directly by the
JavaScript runtime engine, introducing untrusted user input into the
template definition—rather than the data object—creates a critical
dynamic template injection vulnerability that can lead to Remote Code
Execution (RCE).
Eliminating
Scope Leakage with options.variable
By default, Lodash compiles templates using a JavaScript
with (obj || {}) { ... } block to allow direct access to
properties passed within the data object. The with
statement dynamically alters the lexical scope, which introduces
performance penalties and enables scope-chain traversal exploits if
dynamic input reaches object properties.
To mitigate strictly dynamic scope injection, Lodash provides the
options.variable configuration. Specifying an explicit
variable name:
const compiled = _.template(templateString, { variable: 'data' });This configuration disables the generation of the with
statement entirely. Lodash transforms internal property access to
explicit lookups against the specified parameter (e.g.,
data.user instead of user). This restricts the
template’s execution context, preventing dynamic variables from leaking
into or overriding higher-order execution scopes.
Restricting Delimiters Dynamically
A primary vector for injection is delimiter breakout, where an
attacker injects closing delimiters (like %>) to
prematurely close an interpolation block and append unauthorized
JavaScript statements.
Lodash allows programmatic overriding of its default regular
expressions (_.templateSettings.interpolate,
_.templateSettings.escape, and
_.templateSettings.evaluate). To prevent dynamic
injection:
- Disable Evaluation: Nullify the
evaluatesetting dynamically if code execution is not strictly required:const safeSettings = { evaluate: /(?!)GenericImpossibleRegex/ }; - Strict Delimiter Enforcement: Use rigid regex boundaries that ensure delimiters only capture anticipated alphanumeric or safe character formats, disallowing dynamic payload strings containing quotes, semicolons, or execution operators.
Addressing SourceURL and Dynamic Payload Sanitization
In earlier versions of Lodash, dynamic properties like
options.sourceURL could be exploited to escape template
definitions via newline injection (such as CVE-2021-23337). Lodash
addressed this by applying strict sanitization to internal options
passed during template creation.
Lodash dynamically sanitizes internal strings by escaping control
characters, carriage returns (\r), line feeds
(\n), and backslashes before concatenating them into the
executable string passed to the Function constructor. This
prevents attackers from terminating dynamic string literals within the
generated function body.
Enforcement of Context Isolation
While Lodash provides internal escaping through its HTML entity map
(_.escape), secure execution in multi-tenant or dynamic
runtime environments requires strict boundaries:
- Separate Template from Data: Untrusted input must only ever be passed as the runtime data object, never as the template definition itself.
- Object Freezing: Lock down
_.templateSettingsusingObject.freeze()to prevent prototype pollution attacks from dynamically modifying template delimiters across the entire Node.js process. - Runtime Sandboxing: When user-defined templates
must be executed, isolate the generated template function within an
isolated virtual machine or environment (such as
isolated-vm) to restrict access to global objects likeprocess,require, orglobalThis.