vindeckyy opened a new issue, #4425: URL: https://github.com/apache/logging-log4j2/issues/4425
## Description At configuration load/reload time, Log4j substitutes configuration values — including **every plugin attribute** — with a substitutor that resolves lookups against `event == null`. Among the reachable lookups is `ctx` (`ContextMapLookup`), whose no-event variant `lookup(String)` reads the **context data of whichever thread happens to load or reload the configuration** (`ContextDataInjectorFactory.createInjector().getValue(key)`, `ContextMapLookup.java:42-44` in `log4j-core` 2.26.1). Apache's own threat model states that Log4j *"does not trust the values stored in the thread context"* (<https://logging.apache.org/log4j/2.x/security.html>). When an application stores request-derived data (HTTP header, query parameter, user name) in the Thread Context Map before Log4j initializes or before a reconfiguration, that untrusted data is substituted into configuration values: - If the configuration interpolates a lookup into a script attribute (`<Script scriptText="${ctx:…}"/>`, `ScriptFile path="${ctx:…}"`), the attacker's value becomes the script source / script path that `ScriptManager` compiles and runs in the application JVM. - In any layout pattern that references a lookup, the resolved value is **baked into the pattern**: on 2.26.1 a layout containing `${env:HOME}` was measured to resolve to `/home/hayden` at pattern-construction time, so the value is written to every subsequent log line. (On ≤ 2.23.x the recursive evaluation inside lookup results additionally propagated nested lookups; since 2.24.x `LookupResult.isLookupEvaluationAllowedInValue()` defaults to `false`, so the top-level resolution above is the remaining path.) Neither trigger produces a warning: the substitution behaves as ordinary configuration-driven behavior. ### Configuration-time substitution always runs with `event == null` Every configuration-value substitution path reaches `StrLookup.lookup(String)` (the no-event variant): - `config/xml/XmlConfiguration.java:111`, `config/json/JsonConfiguration.java:72`, `config/composite/CompositeConfiguration.java:82` — property replacement with `getConfigurationStrSubstitutor().replace(entry.getValue())` - `config/plugins/util/PluginBuilder.java:190-194` — **all plugin attributes** are substituted with the configuration substitutor when `event == null` - `config/Property.java:143` — `Property.createProperty()` substitutes with the runtime substitutor and a `null` event The event-aware variant `ContextMapLookup.lookup(LogEvent, String)` is the correct per-event path (`event.getContextData().getValue(key)`) and returns `null` for a `null` event. Only the no-event variant drifts to ambient thread state. ### Triggers - The MDC is populated before Log4j initializes (the configuration is loaded at the first logger use), or - The configuration is reloaded while the MDC holds request data: `Configurator.reconfigure()`, `monitorInterval`, a management endpoint, a plugin reload. ## Affected configuration **Version:** `log4j-core` 2.14.1 – 2.26.1 (the current release); the identical code shape exists in the 3.x line (`ContextMapLookup.lookup(String)` is unchanged in 3.0.0-beta3) — source-level comparison only, not executed. Verified vulnerable by execution: 2.14.1, 2.15.0, 2.16.0, 2.17.1, 2.24.0, 2.25.1, 2.26.1. `log4j-api` alone is not affected (no lookup engine). Exploitability conditions per impact: | impact | what the target needs | | --- | --- | | script attribute → code execution | scripting enabled (`log4j2.Script.enableLanguages` + a JSR-223 engine) **and** a configuration that interpolates a lookup into a script attribute (`scriptText=`, `path=`, `fileName=`) | | value baked into layouts | any configuration that references a lookup in a layout (`${ctx:…}`, `${env:…}`, `${sys:…}`) and untrusted data present in the context map at configuration load/reload time | The default `log4j2.xml` configuration is **not** applicable to the script-attribute impact (no script attributes by default); the baking impact needs no special configuration. **Operating system:** Linux **JDK:** OpenJDK 17.0.21 ## Reproduction An isolated, localhost-only reproduction (victim app with the configuration above, attacker data in the MDC, measured output) is in the public write-up: <https://github.com/vindeckyy/log4j-config-time-lookup-rce> (`lab/run_script_rce.sh 2.26.1 startup`). Measured on 2.26.1: ```text === PAYLOAD EXECUTED IN VICTIM JVM (script) === RCE via log4j script (config-time lookup evaluation) as hayden pid=9285 uid=1000(hayden) gid=1000(hayden) groups=1000(hayden),4(adm),24(cdrom),27(sudo),... ``` and for the baking impact: | lookup in layout | resolves to at configuration time (measured, 2.26.1) | | --- | --- | | `${env:HOME}` | `/home/hayden` | | `${sys:user.name}` | `hayden` | | `${java:version}` | `Java version 17.0.21-ea` | A JUnit test sketch matching the code base's test style: ```java @Test void configurationSubstitutionDoesNotReadAmbientContext() { ThreadContext.put("k", "ambient-value"); try { final ConfigurationStrSubstitutor s = new ConfigurationStrSubstitutor(new Interpolator()); // must not resolve to "ambient-value" assertEquals("${ctx:k}", s.replace("${ctx:k}")); } finally { ThreadContext.clearAll(); } } @Test void eventAwareLookupStillResolvesPerEvent() { final LogEvent event = Log4jLogEvent.newBuilder() /* …setContextData(k=v)… */ .build(); final RuntimeStrSubstitutor s = new RuntimeStrSubstitutor(new Interpolator()); assertEquals("v", s.replace(event, "${ctx:k}")); } ``` ## Proposed fix `${ctx:…}` is per-event data. Without an event there is nothing legitimate to resolve — and the event-aware variant of the same class already treats a `null` event as "no context data available" (`ContextMapLookup.java:54`). The no-event variant should behave identically instead of reading the calling thread's (possibly attacker-controlled) context: ```java /** * Looks up the value from the context data of the supplied event. */ @Override public String lookup(final String key) { // ${ctx:…} is per-event data; without an event there is no value to resolve. // Resolving the context map of whichever thread loads or reloads the configuration // is undefined behavior (see https://logging.apache.org/log4j/2.x/security.html: // thread context values are untrusted) and allowed untrusted data into // configuration attributes. return null; } ``` Consequences, all improvements over the current behavior: 1. The script-attribute chain fails closed: `${ctx:…}` stays unresolvable at configuration time, so no attacker-dependent script source/path is ever constructed. 2. Layout patterns no longer freeze a snapshot of ambient context data into the pattern. 3. `<Property value="${ctx:…}">` gains the *correct* per-event semantics: the literal survives configuration-time substitution, and is evaluated per event when referenced via `${prop:…}` through `PropertiesLookup`'s nested evaluation (the event flows into the re-evaluation). 4. `appender/rewrite/PropertiesRewritePolicy.java:79` currently passes no event either; passing `source` there (`config.getStrSubstitutor().replace(source, prop.getValue())`) resolves `${ctx:…}` against the event being rewritten (correct for asynchronous logging, where the ambient map of the writing thread is unrelated to the event). 5. Null-event runtime sites such as `pattern/LiteralPatternConverter.java:71-79` (serializer paths with no event) become consistent: unresolved instead of "whatever the calling thread had in its context map". Two details worth keeping from the current design: - `%X{key}` (via the context-map pattern converters) is unaffected and remains the per-event idiom for layouts; the manual should continue to recommend it. - The change does not address `${sys:…}` / `${env:…}` at configuration time — those are operator-controlled inputs and legitimate configuration features; the defect is only about per-thread, untrusted data. Documentation: the *Lookups* page (and `ConfigurationStrSubstitutor`'s javadoc, which currently describes a "recursive evaluation" policy that since 2.24 lives per-lookup-result in `LookupResult`) should state that context-data lookups are evaluated per event and are not available during configuration substitution. ## Disclosure note This finding was found in 2026-10-23 and published together with its reproduction lab; the full analysis with `file:line` evidence for `log4j-core` and the JDK, the measured exploitability matrix across 2.14.1–2.26.1, detection and remediation guidance is public at <https://github.com/vindeckyy/log4j-config-time-lookup-rce> (see `DISCLOSURE.md` there: one report covering the two findings from that audit is in progress with the Apache Logging Services security process). I'm happy to help test a fix, and I can provide the patch as a PR if the project wants it — happy to adjust the patch shape to whatever the maintainers prefer. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
