Safeguard
Vulnerability Management

Text4Shell deep dive: how CVE-2022-42889 turned string formatting into RCE

CVSS 9.8. Apache Commons Text 1.5–1.9 ran attacker strings through a script interpolator by default — here's the root cause and the fix.

Safeguard Research Team
Research
6 min read

On October 13, 2022, NVD published CVE-2022-42889 — a CVSS 9.8 critical code-injection flaw in Apache Commons Text that researchers immediately nicknamed "Text4Shell" for its resemblance to the Log4Shell disaster eleven months earlier. The bug lived in every release from 1.5 through 1.9: Commons Text's StringSubstitutor class resolved "${prefix:name}" interpolation syntax against a default StringLookupFactory registry that included, out of the box, a script lookup capable of executing JVM scripting-engine expressions, plus dns and url lookups that would silently reach out to attacker-chosen hosts. Any application that ran untrusted input — a form field, a config value, an HTTP parameter — through StringSubstitutor.createInterpolator() or an equivalent default configuration was exploitable for arbitrary code execution, CWE-94, with no authentication and no user interaction required (AV:N/AC:L/PR:N/UI:N). Apache shipped the fix in Commons Text 1.10.0, disabling the dangerous interpolators by default and requiring an explicit opt-in to re-enable them. This post walks through why the defaults were dangerous in the first place, how the exploitation chain actually worked, why it diverges from Log4Shell despite the comparisons, and what a durable remediation looks like.

What exactly made StringSubstitutor dangerous by default?

The danger was in the registry, not in any single line of application code. StringLookupFactory — the class StringSubstitutor.createInterpolator() wires up automatically — registered a set of named lookups keyed by prefix: ${script:...} routed through javax.script.ScriptEngineManager to execute JavaScript (or any installed JSR-223 engine) directly against the interpolated string; ${dns:...} performed live DNS resolution; ${url:...} fetched a remote URL and interpolated the response body. None of these three requires special configuration to activate — they existed in the default lookup map returned by StringLookupFactory.INSTANCE.stringLookupMap(), and any consumer of the convenience constructor inherited all of them. A developer who called StringSubstitutor.createInterpolator().replace(userInput) to do harmless variable substitution — say, expanding ${sys:os.name} in a log template — got script execution and network-lookup interpolators bundled in for free, whether or not that developer's code ever needed them.

How does the actual exploitation path work?

Exploitation required only that attacker-controlled text reach a StringSubstitutor instance built with the default lookup map. A payload like ${script:javascript:java.lang.Runtime.getRuntime().exec('id')} — or the equivalent using java.lang.ProcessBuilder — parsed as a nested interpolation: the script prefix told StringLookupFactory to hand the remainder to a JVM ScriptEngine, which then evaluated arbitrary JavaScript with the full permissions of the host JVM process. Packet Storm's public write-up on the disclosure demonstrated exactly this pattern against applications that piped request parameters, uploaded metadata, or config values into StringSubstitutor.replace(). Because Commons Text is a low-level utility library pulled in transitively by dozens of frameworks and internal tools for templating and config-expansion helpers, the vulnerable call often sat several dependency layers away from the code an application team actually wrote and reviewed — meaning many teams didn't know they shipped it at all.

Is this the same bug as Log4Shell?

No — the mechanism is different even though the outcome and the "any string can become code" pattern rhyme. Log4Shell (CVE-2021-44228) abused Log4j's JNDI lookup to fetch and deserialize a remote Java class over LDAP or RMI, turning a logging call into remote class loading. Text4Shell instead abused Commons Text's script interpolator to hand a string straight to a JVM scripting engine — no remote class loading or JNDI involved, and no network round-trip required for the script variant specifically (though dns and url do reach out externally). Both bugs share the same root pattern that NVD's own description highlights: a general-purpose string-interpolation or lookup mechanism was wired, by default, to interpreters powerful enough to execute code or contact arbitrary hosts, and neither library initially treated "untrusted input reaching this API" as the threat model it needed to be designed against.

What did Apache change in the fix, and is upgrading enough?

Commons Text 1.10.0 disabled script, dns, and url from the default interpolator set entirely — an application now has to explicitly opt back in to any of them via StringLookupFactory if it genuinely needs that behavior, which most consumers of the library never did. Upgrading to 1.10.0 or later closes the hole for direct dependents immediately. The harder problem is transitive exposure: Commons Text ships inside many frameworks, build plugins, and internal utility jars, so an application can be "fixed" at the top level while a nested, unpinned copy of 1.5–1.9 rides along inside another dependency's shaded jar. Vendor advisories from NetApp, SonicWall, and Gentoo in the weeks following disclosure each confirmed downstream products bundling vulnerable Commons Text versions independently of the applications that used them, underscoring that a single pom.xml version bump doesn't guarantee remediation across a real dependency graph.

Why did some scanners under-prioritize this CVE?

Text4Shell is a good stress test for reachability-based triage precisely because it's data-driven rather than purely call-graph-driven: the vulnerable method (StringSubstitutor.replace()) is often "reachable" in the trivial sense that an application calls it constantly for legitimate templating, so a naive reachability check can't distinguish a safe call over trusted, hardcoded template strings from a dangerous one over attacker-supplied input. A scanner that only asks "is this function called" will mark almost every Commons Text consumer as reachable and therefore urgent, while one that can't reason about which strings flow into the call risks the opposite mistake — treating it as low-priority because the sink function itself isn't unusual. The right signal set for a CVE like this is CVSS severity plus KEV/EPSS exploitation data plus an explicit check for whether the interpolator was left at default configuration, since reachability alone — as vulnerability-prioritization research generally acknowledges for data-driven flaws — can't fully substitute for knowing whether the input path is trusted.

How Safeguard helps

Safeguard's SCA engine resolves the full transitive dependency graph, including nested copies of Commons Text shaded inside other libraries, and matches every resolved version against CVE-2022-42889 with EPSS and KEV context attached, then surfaces the safe upgrade target of 1.10.0 or later automatically. Because this CVE is a known case where reachability alone under-prioritizes risk — the vulnerable call is often legitimately reachable, and the real question is whether untrusted input reaches it — Safeguard's prioritization engine weights CVSS severity and exploitability signals rather than suppressing the finding purely on a call-graph verdict, so a Text4Shell-class finding doesn't quietly fall into a backlog just because StringSubstitutor.replace() gets called on every request.

Never miss an update

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