Safeguard
Vulnerability Analysis

underscore.js template code injection (CVE-2021-23358)

CVE-2021-23358 lets attacker-controlled template settings inject arbitrary code via underscore.js's _.template function. Here's the impact, fix, and remediation steps.

Nayan Dey
Security Researcher
7 min read

A code injection vulnerability in the _.template function of underscore.js — tracked as CVE-2021-23358 — allows an attacker who can influence template compilation settings to execute arbitrary JavaScript in the context of the running application. Because underscore.js compiles templates by generating a JavaScript function body as a string and passing it to new Function(...), any unsanitized input that reaches the template settings object can be smuggled directly into executable code. In a Node.js service, that translates to remote code execution; in a browser context rendering user- or tenant-supplied templates, it translates to cross-site scripting or full client-side compromise. Given how many frontend frameworks, build tools, and legacy Backbone.js applications still pull in underscore.js — often as a transitive dependency several layers deep — this is the kind of bug that quietly outlives the projects that introduced it.

What's actually vulnerable

The flaw lives in underscore's template engine, specifically the handling of the variable option inside templateSettings. When _.template() builds the compiled function, it interpolates the caller-supplied variable name directly into the generated function source rather than validating that it is a safe, bare JavaScript identifier. If an application allows any part of that settings object — most commonly the variable field, though other template configuration surfaces have shown similar weaknesses — to be influenced by external input (a query parameter, a stored configuration value, a multi-tenant customization field), an attacker can inject a payload like x;alert(1);// and have it evaluated as live code the moment the template is compiled and rendered.

This is conceptually the same class of bug as the template-injection issues that have periodically surfaced in lodash's _.template — the underlying pattern of "build a function body from a string, don't sanitize the pieces you're allowed to override" recurs across the ecosystem because compiled templating is a convenient performance trick that trades away input hygiene.

Affected versions and components

  • Vulnerable: underscore.js versions prior to 1.13.0-2
  • Fixed: 1.13.0-2 and later (the maintainers added a regex check requiring variable to match a valid identifier pattern, throwing an error otherwise)
  • Package: underscore on npm (GHSA-cf4h-3jhx-xvhq)
  • Exposure path: any application, library, or build tool that calls _.template() with a templateSettings object whose fields (particularly variable) are derived, even indirectly, from user-controllable input

The tricky part for defenders is that underscore is rarely a direct dependency of modern applications — it's far more often pulled in transitively through UI component libraries, legacy Backbone.js scaffolding, CLI tooling, or older build pipelines. That means a straightforward npm ls underscore at the top level frequently misses it entirely, and teams don't discover they're exposed until a dependency graph tool or an SBOM diff surfaces the nested version.

CVSS, EPSS, and KEV context

NVD scores CVE-2021-23358 as 9.8 (Critical) under CVSS v3.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H), reflecting the worst-case outcome — network-reachable arbitrary code execution with no privileges or user interaction required. That score is worth reading with a grain of context, though: it assumes an application actually routes untrusted input into the vulnerable templateSettings.variable field, which is a real but not universal pattern. Plenty of applications that carry a vulnerable underscore.js version never call _.template() with attacker-influenced settings at all, and for them the practical exploitability is close to zero even though the dependency shows up as "critical" in a naive SCA scan. This gap between theoretical CVSS and actual reachability is exactly the kind of noise that burns triage time across security teams.

EPSS scoring for this CVE has stayed low relative to its CVSS severity, consistent with limited observed active exploitation in the wild — template-injection bugs of this shape tend to require specific application logic to be reachable, which naturally suppresses mass-exploitation activity compared to, say, an unauthenticated deserialization bug. As of this writing, CVE-2021-23358 does not appear on CISA's Known Exploited Vulnerabilities (KEV) catalog. That said, absence from KEV and a modest EPSS score should not be read as "safe to ignore" — they describe internet-wide exploitation patterns, not whether your specific codebase happens to wire attacker input into a template variable name.

Timeline

  • Early underscore.js history: The vulnerable template-compilation pattern (interpolating the variable setting unsanitized into generated function source) has existed in the library's templating implementation for years, inherited from the same general design as underscore's original template engine.
  • January 2021: The issue is identified and responsibly reported to the underscore.js maintainers; a fix is developed that validates variable against an identifier-safe regular expression before it can reach the generated function body.
  • Late January 2021: underscore.js 1.13.0-2 ships with the fix. The GitHub Security Advisory GHSA-cf4h-3jhx-xvhq is published shortly after, describing the arbitrary code execution risk.
  • Q1 2021: CVE-2021-23358 is formally registered and populated in NVD with the critical severity score, and the advisory begins propagating through SCA tooling and dependency scanners.
  • Ongoing: Because underscore is so frequently a transitive dependency, downstream packages and applications continue to carry vulnerable pinned versions for months to years after the fix shipped — a familiar long tail for widely embedded utility libraries.

Remediation steps

  1. Upgrade underscore.js to 1.13.0-2 or later (the current release line is safe). If you depend on it directly, bump the version in package.json and regenerate your lockfile; if it's transitive, use npm ls underscore / yarn why underscore or your SBOM to find every version in the tree and force a resolution override where a direct upgrade isn't available.
  2. Audit every _.template() call site for cases where templateSettings — especially the variable, escape, interpolate, or evaluate fields — is built from configuration that a user, tenant, or upstream API response can influence. Treat any such call as a code-injection sink, not just a formatting helper.
  3. Refactor to hardcode template settings where possible rather than accepting them as configurable input. If per-tenant customization is a genuine product requirement, validate any user-influenced identifier against a strict allowlist regex before it ever reaches the template compiler.
  4. Check for related exposure in lodash's _.template, since applications that use one utility-belt library frequently use the other, and the underlying risk pattern is structurally similar.
  5. Add defense-in-depth controls for any template rendering that touches untrusted data: a strict Content Security Policy for browser-rendered templates, and process-level sandboxing or restricted execution contexts for server-side template compilation.
  6. Write a regression test that asserts _.template() throws (rather than silently compiling) when given a non-identifier variable value, so a future dependency downgrade or refactor doesn't silently reopen the hole.
  7. Re-scan after patching. Because underscore shows up nested at multiple depths in many dependency trees, a single top-level bump doesn't guarantee every copy is remediated — verify with a full SBOM diff, not a spot check.

How Safeguard Helps

Safeguard's reachability analysis traces whether your application's actual call graph ever routes untrusted input into a vulnerable _.template() invocation, so you can tell the difference between a critical-by-CVSS finding that's genuinely exploitable in your code and one that's dead weight sitting in an unused code path — cutting through exactly the CVSS-versus-reality gap CVE-2021-23358 illustrates. Griffin AI goes a step further by following the data flow from an untrusted input source through your codebase to confirm whether it can reach a templateSettings sink, giving your team evidence-backed prioritization instead of a flat severity score. Because underscore.js is so often buried several layers deep as a transitive dependency, Safeguard's SBOM generation and ingestion capabilities surface every occurrence across your entire fleet of repositories in one pass, including nested copies that manual npm ls checks routinely miss. When a fix is confirmed, Safeguard can open an auto-fix pull request that bumps underscore to a patched release and updates the lockfile, so remediation moves from a backlog ticket to a mergeable change in minutes.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.