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]

Reply via email to