← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
CVE-2026-4800 · CWE-94 · Disclosed 2026-03-31

Impact: The fix for CVE-2021-23337

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
01 · The Real Story

This is a loaded nail gun left in the workshop, not a sniper rifle pointed at the internet

CVE-2026-4800 is a code-injection flaw in Lodash _.template. Affected ranges are lodash, lodash-es, and lodash-amd >=4.0.0 through <=4.17.23, plus lodash.template >=4.0.0 and <4.18.0; the fix lands in 4.18.0. The bug exists because the old hardening for CVE-2021-23337 validated the variable option but did not validate options.imports key names, even though both paths feed the same Function() constructor sink.

Vendor HIGH is technically defensible for reachable cases, but it overstates broad enterprise risk. This is not a generic internet-reachable daemon bug; exploitation usually requires a very specific developer mistake—passing attacker-controlled keys into _.template options—or a chain with separate prototype pollution. For a 10,000-host fleet, that turns this from an emergency patch-everything event into a targeted hunt for reachable usage.

"Serious if reachable, but most fleets only lose when app code hands attacker input to _.template internals."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Find a reachable _.template sink

The attacker first needs an application path that calls Lodash _.template with a controllable second argument, especially options.imports, or a code path where Object.prototype can already be polluted. The vulnerable behavior lives in library code, but reachability is entirely application-dependent. Weaponization starts with ordinary HTTP requests plus code-awareness, not mass scanning.
Conditions required:
  • Target uses vulnerable Lodash package/version
  • Application actually calls _.template
  • Attacker can influence options.imports keys or earlier prototype state
Where this breaks in practice:
  • Most apps use Lodash helpers without ever exposing _.template to user input
  • Many modern JS stacks do not use runtime template compilation at all
  • This often requires source-level understanding or a second bug
Detection/coverage: SCA tools (npm audit, Dependabot, Trivy, Grype, osv-scanner) will flag vulnerable versions, but they cannot prove exploit reachability. Code search and taint analysis are the real detectors here.
STEP 02

Inject a malicious imports key

Using a PoC such as threalwinky/CVE-2026-4800-POC, the attacker supplies a crafted imports key name that abuses default-parameter syntax so the generated function body becomes executable JavaScript. The alternate path is to pollute Object.prototype and let Lodash copy inherited properties into imports through the old merge behavior.
Conditions required:
  • Attacker controls imports key names or can pollute Object.prototype first
  • Application does not sanitize or freeze that path before calling _.template
Where this breaks in practice:
  • Key-name injection is a niche coding anti-pattern
  • Prototype-pollution chaining assumes another exploitable flaw already exists
  • Input validation, schema enforcement, or use of static imports blocks this step
Detection/coverage: Static analysis can catch _.template(..., userControlledOptions) patterns. Runtime telemetry may show suspicious payload strings in request logs, but coverage is inconsistent.
STEP 03

Trigger compile-time execution in Node.js

When the app compiles the template, Lodash feeds the attacker-influenced identifiers into Function(). That turns the payload into code execution at template compilation time, running with the privileges of the application process. In server-side Node.js deployments, that is immediate app-context RCE.
Conditions required:
  • Compilation path executes on the server
  • Process has enough privileges to matter
Where this breaks in practice:
  • Some uses are client-side only, reducing impact to browser-side script execution
  • Sandboxed runtimes, containers, seccomp, or reduced OS privileges can narrow blast radius
Detection/coverage: EDR can sometimes catch child-process spawn, shell execution, or unusual Node behavior after exploitation. It usually will not identify the Lodash root cause by itself.
STEP 04

Post-exploitation pivot

Once code executes, common follow-on tradecraft is child_process.exec*, credential theft from environment variables, or application data access. The blast radius is bounded by the app account, container permissions, mounted secrets, and network reach from that service.
Conditions required:
  • Successful code execution in a meaningful service context
  • Valuable secrets, data, or lateral paths available from the process
Where this breaks in practice:
  • Least-privilege service accounts and locked-down containers limit follow-on damage
  • Outbound egress controls and secret managers can blunt easy monetization
Detection/coverage: Process creation, unexpected outbound connections, and secret-access anomalies are the practical telemetry points. Signature-only web controls are weak here.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusNo authoritative public evidence of active exploitation found in the sources reviewed. This CVE is not in CISA KEV.
Public PoC availabilityYes. Public GitHub PoC exists at threalwinky/CVE-2026-4800-POC, which demonstrates prototype-pollution-assisted execution through _.template.
EPSS0.00044 from the user-provided intel; that is a very low short-term exploitation probability. FIRST's public EPSS page confirms score semantics, though the percentile was not directly exposed in this browsing workflow.
KEV statusNot listed in the CISA Known Exploited Vulnerabilities Catalog.
CVSS reality checkCNA/OpenJS scores it 8.1 HIGH with AC:H, which fits the prerequisite-heavy path. NVD later enriched it to 9.8 CRITICAL, but that drops the real-world friction and is too aggressive for fleet prioritization.
Affected versionslodash, lodash-es, lodash-amd: >=4.0.0 to <=4.17.23; lodash.template: >=4.0.0 and <4.18.0.
Fixed versionsPrimary upstream fix is 4.18.0. Be careful with distro packages: Ubuntu's node-lodash page still showed Needs evaluation across supported releases when checked, so distro consumers should verify vendor backports rather than only npm upstream versions.
Exposure/scanning realityThis is not directly internet-fingerprintable via Shodan/Censys/GreyNoise because Lodash is an embedded library, not a network service. Exposure has to be derived from SBOM/SCA plus code search for _.template and options.imports usage.
Disclosure timelineOpenJS/NVD show disclosure on 2026-03-31; OSV shows publication on 2026-04-01T23:51:12Z.
Reporter / sourceGitHub advisory was published by Ulises Gascón; CNA source on NVD is openjs.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to MEDIUM (5.6/10)

The decisive downward pressure is reachability: attackers do not get internet-wide RCE just because a host has Lodash installed; they need a specific app pattern that passes untrusted keys into _.template internals or a second bug to pollute prototypes first. That sharply narrows exposed population even though the technical impact, once triggered, is real code execution in the application process.

HIGH Technical impact if the sink is reachable
MEDIUM Fleet-wide prevalence of truly exploitable application paths

Why this verdict

  • Reachability is the whole game: starting from the vendor's 8.1, I subtract because most enterprises have Lodash everywhere but do not expose attacker-controlled options.imports into _.template.
  • The alternate exploit path assumes prior compromise of app logic: the prototype-pollution route is a chain, not a clean standalone remote bug. Requiring a second vulnerability or dangerous coding pattern compounds downward pressure.
  • Threat intel is cold: no KEV, no strong public in-the-wild reporting in reviewed sources, and an EPSS of 0.00044 all argue against treating this like an immediate fleet emergency.

Why not higher?

If this were a default-on network service flaw or broadly reachable request-to-RCE in typical Node stacks, it would stay HIGH or go higher. But it is a library sink gated by uncommon application behavior, and that means the exposed population is much smaller than the package install base.

Why not lower?

I am not pushing this to LOW because successful exploitation is still real code execution with no authentication once the vulnerable application path exists. Internet-facing apps that compile templates from tainted input can go from bug to shell quickly, so this deserves targeted investigation rather than backlog oblivion.

05 · Compensating Control

What to do — in priority order.

  1. Inventory reachable _.template usage — Use code search, Semgrep, or taint analysis to find _.template( calls and whether the second argument can be influenced by request data. For a MEDIUM verdict there is no mitigation SLA, so do this in the next normal engineering cycle and use it to separate harmless transitive presence from real exposure.
  2. Block untrusted imports keys — If immediate upgrade is not possible, enforce developer-controlled static keys for options.imports and reject any request path that maps attacker input into template engine configuration. There is no mitigation SLA for MEDIUM; fold this into the normal remediation plan where reachable usage exists.
  3. Kill the prototype-pollution chain — Patch or disable adjacent prototype-pollution sources that can taint Object.prototype, because this CVE explicitly becomes easier when polluted keys are inherited into template imports. Again, there is no mitigation SLA here; handle it as part of the application hardening workstream.
  4. Constrain Node process blast radius — Run services as non-root, restrict container capabilities, minimize mounted secrets, and tighten egress so app-context RCE does not become instant host or cloud compromise. This does not remove the bug, but it materially reduces impact while you move through the normal patch window.
What doesn't work
  • A generic WAF does not solve this; the payload is meaningful only when your application feeds it into _.template, and many request shapes will look like ordinary JSON.
  • Version-only asset scanning is insufficient; it tells you where Lodash exists, not whether _.template is reachable from untrusted input.
  • Relying on EDR alone is too late; it may catch child-process behavior after code execution, but it will not reliably prevent the dangerous template compilation path.
06 · Verification

Crowdsourced verification payload.

Run this on the application source tree, build workspace, or target host where the Node.js app and its dependency files live. Invoke it with python3 check_cve_2026_4800.py /path/to/app; no admin rights are required, but the script needs read access to node_modules and common lockfiles.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/usr/bin/env python3
# CVE-2026-4800 verifier for Lodash packages
# Usage: python3 check_cve_2026_4800.py /path/to/app
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN

import json
import os
import re
import sys
from typing import Optional, Tuple, List

TARGETS = {
    'lodash': (4, 18, 0),
    'lodash-es': (4, 18, 0),
    'lodash-amd': (4, 18, 0),
    'lodash.template': (4, 18, 0),
}

Version = Tuple[int, int, int]


def parse_version(raw: str) -> Optional[Version]:
    if not raw:
        return None
    m = re.search(r'(\d+)\.(\d+)\.(\d+)', raw)
    if not m:
        return None
    return (int(m.group(1)), int(m.group(2)), int(m.group(3)))


def is_vulnerable(pkg: str, ver: Version) -> bool:
    fixed = TARGETS[pkg]
    return ver >= (4, 0, 0) and ver < fixed


def load_json(path: str):
    try:
        with open(path, 'r', encoding='utf-8') as f:
            return json.load(f)
    except Exception:
        return None


def check_node_modules(root: str) -> List[str]:
    findings = []
    nm = os.path.join(root, 'node_modules')
    if not os.path.isdir(nm):
        return findings
    for pkg in TARGETS:
        pkg_json = os.path.join(nm, pkg, 'package.json')
        data = load_json(pkg_json)
        if not data:
            continue
        ver = parse_version(str(data.get('version', '')))
        if not ver:
            findings.append(f'UNKNOWN {pkg} version unreadable in node_modules')
            continue
        if is_vulnerable(pkg, ver):
            findings.append(f'VULNERABLE {pkg} {data.get("version")} via node_modules')
        else:
            findings.append(f'PATCHED {pkg} {data.get("version")} via node_modules')
    return findings


def walk_pkg_lock_packages(packages_obj) -> List[str]:
    findings = []
    if not isinstance(packages_obj, dict):
        return findings
    for path_key, meta in packages_obj.items():
        if not isinstance(meta, dict):
            continue
        name = meta.get('name')
        if not name and path_key.startswith('node_modules/'):
            name = path_key.split('node_modules/', 1)[1]
        if name not in TARGETS:
            continue
        ver = parse_version(str(meta.get('version', '')))
        if not ver:
            findings.append(f'UNKNOWN {name} version unreadable in package-lock packages')
            continue
        if is_vulnerable(name, ver):
            findings.append(f'VULNERABLE {name} {meta.get("version")} via package-lock packages')
        else:
            findings.append(f'PATCHED {name} {meta.get("version")} via package-lock packages')
    return findings


def walk_dependency_tree(deps) -> List[str]:
    findings = []
    if not isinstance(deps, dict):
        return findings
    stack = list(deps.items())
    while stack:
        name, meta = stack.pop()
        if not isinstance(meta, dict):
            continue
        if name in TARGETS:
            ver = parse_version(str(meta.get('version', '')))
            if not ver:
                findings.append(f'UNKNOWN {name} version unreadable in dependency tree')
            elif is_vulnerable(name, ver):
                findings.append(f'VULNERABLE {name} {meta.get("version")} via dependency tree')
            else:
                findings.append(f'PATCHED {name} {meta.get("version")} via dependency tree')
        child = meta.get('dependencies')
        if isinstance(child, dict):
            stack.extend(child.items())
    return findings


def check_package_lock(root: str) -> List[str]:
    findings = []
    for filename in ('package-lock.json', 'npm-shrinkwrap.json'):
        path = os.path.join(root, filename)
        data = load_json(path)
        if not data:
            continue
        findings.extend(walk_pkg_lock_packages(data.get('packages')))
        findings.extend(walk_dependency_tree(data.get('dependencies')))
    return findings


def dedupe(findings: List[str]) -> List[str]:
    seen = set()
    out = []
    for item in findings:
        if item not in seen:
            seen.add(item)
            out.append(item)
    return out


def main() -> int:
    if len(sys.argv) != 2:
        print('UNKNOWN - usage: python3 check_cve_2026_4800.py /path/to/app')
        return 2

    root = sys.argv[1]
    if not os.path.isdir(root):
        print(f'UNKNOWN - path not found: {root}')
        return 2

    findings = []
    findings.extend(check_node_modules(root))
    findings.extend(check_package_lock(root))
    findings = dedupe(findings)

    if not findings:
        print('UNKNOWN - no target packages found in node_modules or npm lockfiles')
        return 2

    vuln = [f for f in findings if f.startswith('VULNERABLE ')]
    patched = [f for f in findings if f.startswith('PATCHED ')]

    if vuln:
        print('VULNERABLE - ' + '; '.join(vuln))
        return 1
    if patched:
        print('PATCHED - ' + '; '.join(patched))
        return 0

    print('UNKNOWN - ' + '; '.join(findings))
    return 2


if __name__ == '__main__':
    sys.exit(main())
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning, do not launch an all-hands emergency just because Lodash is present everywhere. Triage this as a reachability problem: use SCA/SBOM to find vulnerable packages, then code-search for _.template with tainted options usage in internet-facing Node services first. For a MEDIUM verdict, the noisgate mitigation SLA is no mitigation SLA — go straight to the 365-day remediation window, and the noisgate remediation SLA is to upgrade reachable and remaining vulnerable packages to >=4.18.0 within 365 days; if you discover an actually reachable untrusted-template path, accelerate that application outside the generic fleet clock.

Sources

  1. NVD CVE record
  2. GitHub security advisory GHSA-r5fr-rjxr-66jc
  3. Lodash 4.18.0 release notes
  4. OSV entry for GHSA-r5fr-rjxr-66jc
  5. FIRST EPSS data and statistics
  6. CISA Known Exploited Vulnerabilities Catalog
  7. Ubuntu CVE tracker entry
  8. Public PoC repository
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.