Incomplete fix for Apache Log4j vulnerability
Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.
Affected versions
Details
# Impact The fix to address [CVE-2021-44228](https://nvd.nist.gov/vuln/detail/CVE-2021-44228) in Apache Log4j 2.15.0 was incomplete in certain non-default configurations. This could allow attackers with control over Thread Context Map (MDC) input data when the logging configuration uses a non-default Pattern Layout with either a Context Lookup (for example, $${ctx:loginId}) or a Thread Context Map pattern (%X, %mdc, or %MDC) to craft malicious input data using a JNDI Lookup pattern resulting in a remote code execution (RCE) attack. ## Affected packages Only the `org.apache.logging.log4j:log4j-core` package is directly affected by this vulnerability. The `org.apache.logging.log4j:log4j-api` should be kept at the same version as the `org.apache.logging.log4j:log4j-core` package to ensure compatability if in use. # Mitigation Log4j 2.16.0 fixes this issue by removing support for message lookup patterns and disabling JNDI functionality by default. This issue can be mitigated in prior releases (< 2.16.0) by removing the JndiLookup class from the classpath (example: zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class). Log4j 2.15.0 restricts JNDI LDAP lookups to localhost by default. Note that previous mitigations involving configuration such as to set the system property `log4j2.formatMsgNoLookups` to `true` do NOT mitigate this specific vulnerability.
The fix
Create branch for security releases
src/site/xdoc/index.xml+65 −1
@@ -27,7 +27,71 @@</properties><body>-<section name="Apache Log4j 2">++<a name="CVE-2021-45105"/>+<h2>Important: Security Vulnerabilities CVE-2021-45105, CVE-2021-45046 and CVE-2021-44228</h2>++<p>The Log4j team has been made aware of multiple security vulnerabilities, CVE-2021-45105, CVE-2021-45046 and CVE-2021-44228,+that have been addressed in Log4j 2.3.1 for Java 6.+The same vulnerabilities have been addressed in Log4j 2.12.3 for Java 7, and in+Log4j 2.17.0 for Java 8 and up.</p>++<h3>CVE-2021-45105</h3>+<p>Summary: Apache Log4j2 does not always protect from infinite recursion in lookup evaluation.</p>++<h4>Details</h4>+<p>Apache Log4j2 versions 2.0-alpha1 through 2.16.0 did not protect from uncontrolled recursion from self-referential lookups.+When the logging configuration uses a non-default Pattern Layout with a Context Lookup (for example, <code>$${ctx:loginId}</code>),+attackers with control over Thread Context Map (MDC) input data can craft malicious input data that contains a recursive lookup,+resulting in a StackOverflowError that will terminate the process. This is also known as a DOS (Denial of Service) attack.</p>++<h4>Mitigation</h4>+<p>Upgrade to Log4j 2.3.1 (for Java 6), 2.12.3 (for Java 7), or 2.17.0 (for Java 8).</p>++<h4>Reference</h4>+<p>Please refer to the <a href="https://logging.apache.org/log4j/2.x/security.html#CVE-2021-45105">Security page</a> for details and mitigation measures for older versions of Log4j.</p>+++<a name="CVE-2021-45046"/>+<h3>CVE-2021-45046</h3>++<p>Summary: Apache Log4j2 Thread Context Lookup Pattern vulnerable to remote code execution in certain non-default configurations.</p>++<h4>Details</h4>+<p>It was found that the fix to address CVE-2021-44228 in Apache Log4j 2.15.0 was incomplete in certain non-default configurations.+When the logging configuration uses a non-default Pattern Layout with a Context Lookup (for example, <code>$${ctx:loginId}</code>),+attackers with control over Thread Context Map (MDC) input data can craft malicious input data using a JNDI Lookup pattern,+resulting in an information leak and remote code execution in some environments and local code execution in all environments;+remote code execution has been demonstrated on macOS but no other tested environments.</p>++<h4>Mitigation</h4>+<p>Upgrade to Log4j 2.3.1 (for Java 6), 2.12.3 (for Java 7), or 2.17.0 (for Java 8).</p>++<h4>Reference</h4>+<p>Please refer to the <a href="https://logging.apache.org/log4j/2.x/security.html#CVE-2021-45105">Security page</a> for details and mitigation measures for older versions of Log4j.</p>+++<a name="CVE-2021-44228"/>+<h3>CVE-2021-44228</h3>++<p>Summary:+Log4j’s JNDI support has not restricted what names could be resolved. Some protocols are unsafe or can allow remote code+execution.</p>++<h4>Details</h4>+<p>One vector that allowed exposure to this vulnerability was Log4j’s allowance of Lookups to appear in log messages.+This meant that when user input is logged, and that user input contained a JNDI Lookup pointing to a malicious server,+then Log4j would resolve that JNDI Lookup, connect to that server, and potentially download serialized Java code from+that remote server. This in turn could execute any code during deserialization.+This is known as a RCE (Remote Code Execution) attack.</p>++<h4>Mitigation</h4>+<p>Upgrade to Log4j 2.3.1 (for Java 6), 2.12.3 (for Java 7), or 2.17.0 (for Java 8).</p>++<h4>Reference</h4>+<p>Please refer to the <a href="https://logging.apache.org/log4j/2.x/security.html#CVE-2021-45105">Security page</a> for details and mitigation measures for older versions of Log4j.</p>++<section name="Apache Log4j 2"><p>Apache Log4j 2 is an upgrade to Log4j that provides significant improvements over its predecessor, Log4j.../core/config/AbstractConfiguration.java | 17 +-.../log4j/core/config/AppenderControl.java | 6 +-.../core/config/ConfigurationFactory.java | 3 +-.../core/config/json/JsonConfiguration.java | 2 +-.../config/plugins/util/PluginBuilder.java | 10 +-.../core/config/xml/XmlConfiguration.java | 2 +-.../lookup/ConfigurationStrSubstitutor.java | 63 +++.../log4j/core/lookup/ContextMapLookup.java | 2 +-.../logging/log4j/core/lookup/DateLookup.java | 2 +-.../log4j/core/lookup/Interpolator.java | 2 +-.../core/lookup/RuntimeStrSubstitutor.java | 61 +++.../log4j/core/lookup/StrSubstitutor.java | 396 ++++++++++++------...olverDoesNotEvaluateThreadContextTest.java | 117 ++++++...outingAppenderKeyLookupEvaluationTest.java | 94 +++++.../log4j/core/lookup/StrSubstitutorTest.java | 138 +++++-.../test/resources/log4j-routing-lookup.xml | 39 ++.../src/test/resources/log4j-routing.json | 5 +-.../src/test/resources/log4j-routing.xml | 5 +-.../src/test/resources/log4j-routing2.json | 5 +-.../log4j2-pattern-layout-with-context.xml | 34 ++.../log4j/web/Log4jWebInitializerImpl.java | 3 +-src/changes/changes.xml | 3 +22 files changed, 858 insertions(+), 151 deletions(-)create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ConfigurationStrSubstitutor.javacreate mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/RuntimeStrSubstitutor.javacreate mode 100644 log4j-core/src/test/java/org/apache/logging/log4j/core/PatternResolverDoesNotEvaluateThreadContextTest.javacreate mode 100644 log4j-core/src/test/java/org/apache/logging/log4j/core/appender/routing/RoutingAppenderKeyLookupEvaluationTest.javacreate mode 100644 log4j-core/src/test/resources/log4j-routing-lookup.xmlcreate mode 100644 log4j-core/src/test/resources/log4j2-pattern-layout-with-context.xml
RELEASE-NOTES.txt+20 −38
@@ -1,56 +1,38 @@-Apache Log4j 2.3 RELEASE NOTES+Apache Log4j 2.3.1 RELEASE NOTES-The Apache Log4j 2 team is pleased to announce the Log4j 2.3 release!+The Apache Log4j 2 team is pleased to announce the Log4j 2.3.1 release!Apache log4j is a well known framework for logging application behavior. Log4j 2 is an upgrade toLog4j that provides significant improvements over its predecessor, Log4j 1.x, and providesmany other modern features such as support for Markers, property substitution using Lookups, and asynchronousLoggers. In addition, Log4j 2 will not lose events while reconfiguring.-This is the sixth GA release. It contains several bugfixes and new features.+The major changes contained in this release include:-GA Release 2.3+* Address CVE-2021-45046 and CVE-2021-45105 by disabling recursive evaluation of Lookups during log event processing. Recursive evaluation is still allowed while generating the configuration.+* Adddress CVE-2021-44882 by removing processing of Lookups in the Message Pattern Converter of the Pattern Layout and+preventing JNDI operations to use any protocols other than java.+* The JndiLookup, JndiContextSelector, and JMSAppender now require individual system properties to be enabled.++The JNDI components are now disabled by default and may separately be enabled with three individual properties; log4j2.enableJndiContextSelector, log4j2.enableJndiJms, and log4j2.enableJndiLookup.++GA Release 2.3.1Changes in this version include:New features:-o LOG4J2-984: PatternLayout %highlight to support noConsoleNoAnsi like %style. Thanks to Jonas Höpfner.-o LOG4J2-926: Truncate from the end of text format modifier. Thanks to David Ohana.+o LOG4J2-3198: Pattern layout no longer enables lookups within message text.Fixed Bugs:-o LOG4J2-1009: Incorrectly defined compressionType parameter to GelfLayout. Thanks to Mikael Ståldal.-o LOG4J2-1008: org.apache.logging.log4j.core.config.plugins.util.ResolverUtil.extractPath(URL) incorrectly converts '+' characters to spaces. Thanks to Ralph Goers, Gary Gregory.-o LOG4J2-1007: org.apache.logging.log4j.core.util#fileFromUri(URI uri) incorrectly converts '+' characters to spaces. Thanks to Ralph Goers, Gary Gregory.-o LOG4J2-1003: JUL Logger.throwing is mis-mapped to ERROR when it should be TRACE. Thanks to Dan Armbrust.-o LOG4J2-965: System.out no longer works after the Console appender and JANSI are initialized. Thanks to Khotyn Huang.-o LOG4J2-993: Deadlock would occur if appender thread creates a new Logger during reconfiguration.-o LOG4J2-991: Async root logger config should default includeLocation to false. Thanks to Ryan Rupp.-o LOG4J2-985: AbstractFilter should not implement equals() and hashCode(). Thanks to Sean Dawson.-o LOG4J2-980: Numerical overflow in BurstFilter not handled correctly. Thanks to Mikhail Mazurskiy.-o LOG4J2-981: Incorrect unlock in ProviderUtil. Thanks to Mikhail Mazurskiy.-o LOG4J2-966: KeyStoreConfiguration.createKeyStoreConfiguration() ignores keyManagerFactoryAlgorithm. Thanks to Gary Gregory.-o LOG4J2-976: Using monitorInterval with YAML config file format causes JSONParseException. Thanks to Matt Quinn.-o LOG4J2-964: StringFormattedMessage serialization is incorrect. Thanks to Jonne Jyrylä.-o LOG4J2-947: A new StatusLoggerAdmin listener is added to StatusLogger every time the log is reconfigured. Thanks to Stefan Wehner.-o LOG4J2-968: SyslogLayout contains extra space. Thanks to Paul D Johe.-o LOG4J2-967: log4j2.component.properties not read for all properties. Thanks to Stefan Wehner.-o LOG4J2-971: Another bad priority in Syslog messages. Thanks to Paul D Johe.-o LOG4J2-972: org.apache.logging.log4j.core.net.ssl.TlsSyslogInputStreamReader does not need to create temp Integer objects. Thanks to Gary Gregory.-o LOG4J2-974: Typo in EventLogger documentation. Thanks to Daniel Galán y Martins.--Changes:-o LOG4J2-998: Make org.apache.logging.log4j.core.Logger#updateConfiguration protected. Thanks to Mariano Gonzalez.-o LOG4J2-995: Move UTF-8 constant from Charsets to Constants class. Remove Charsets class.-o LOG4J2-988: Update LMAX Disruptor from 3.3.0 to 3.3.2. Thanks to Gary Gregory.-o LOG4J2-987: Migrate tests from Logback 1.1.2 to 1.1.3. Thanks to Gary Gregory.-o LOG4J2-988: Update tests to use ActiveMQ from 5.10 to 5.11.1. Thanks to Gary Gregory.-o LOG4J2-1004: Update Jackson from 2.5.1 to 2.5.3.-o LOG4J2-1005: Update Slf4j from 1.7.7 to 1.7.12.---Apache Log4j 2.3 requires a minimum of Java 6 to build and run. This will be the last release of Log4j 2 to-support Java 6. Future releases will require a minimum of Java 7.+o LOG4J2-3242: Limit JNDI to the java protocol only. JNDI will remain disabled by default. Rename JNDI enablement property from+'log4j2.enableJndi' to 'log4j2.enableJndiLookup', 'log4j2.enableJndiJms', and 'log4j2.enableJndiContextSelector'.+o LOG4J2-3230: Fix string substitution recursion.++++Apache Log4j 2.3.1 requires a minimum of Java 6 to build and run. It is not expected that any future Java 6+releases will be provided.Basic compatibility with Log4j 1.x is provided through the log4j-1.2-api component, however it does not implement some of thevery implementation specific classes and methods. The package names and Maven groupId have been changed to
log4j-core/src/test/java/org/apache/logging/log4j/core/PatternResolverDoesNotEvaluateThreadContextTest.java+117 −0
@@ -0,0 +1,117 @@+/*+* Licensed to the Apache Software Foundation (ASF) under one or more+* contributor license agreements. See the NOTICE file distributed with+* this work for additional information regarding copyright ownership.+* The ASF licenses this file to You under the Apache license, Version 2.0+* (the "License"); you may not use this file except in compliance with+* the License. You may obtain a copy of the License at+*+* http://www.apache.org/licenses/LICENSE-2.0+*+* Unless required by applicable law or agreed to in writing, software+* distributed under the License is distributed on an "AS IS" BASIS,+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+* See the license for the specific language governing permissions and+* limitations under the license.+*/+package org.apache.logging.log4j.core;++import org.apache.logging.log4j.MarkerManager;+import org.apache.logging.log4j.ThreadContext;+import org.apache.logging.log4j.junit.InitialLoggerContext;+import org.apache.logging.log4j.test.appender.ListAppender;+import org.junit.Before;+import org.junit.ClassRule;+import org.junit.Test;++import java.util.List;++import static org.junit.Assert.assertTrue;+import static org.junit.Assert.assertEquals;++public class PatternResolverDoesNotEvaluateThreadContextTest {+++private static final String CONFIG = "log4j2-pattern-layout-with-context.xml";+private static final String PARAMETER = "user";+private ListAppender listAppender;++@ClassRule+public static InitialLoggerContext context = new InitialLoggerContext(CONFIG);++@Before+public void before() {+listAppender = context.getListAppender("list");+listAppender.clear();+}++@Test+public void testNoUserSet() {+Logger logger = context.getLogger(getClass().getName());+logger.info("This is a test");+List<String> messages = listAppender.getMessages();+assertTrue("No messages returned", messages != null && messages.size() > 0);+String message = messages.get(0);+assertEquals("INFO org.apache.logging.log4j.core." ++"PatternResolverDoesNotEvaluateThreadContextTest ${ctx:user} This is a test", message);+}++@Test+public void testMessageIsNotLookedUp() {+Logger logger = context.getLogger(getClass().getName());+logger.info("This is a ${upper:test}");+List<String> messages = listAppender.getMessages();+assertTrue("No messages returned", messages != null && messages.size() > 0);+String message = messages.get(0);+assertEquals("INFO org.apache.logging.log4j.core." ++"PatternResolverDoesNotEvaluateThreadContextTest ${ctx:user} This is a ${upper:test}", message);+}++@Test+public void testUser() {+Logger logger = context.getLogger(getClass().getName());+ThreadContext.put(PARAMETER, "123");+try {+logger.info("This is a test");+} finally {+ThreadContext.remove(PARAMETER);+}+List<String> messages = listAppender.getMessages();+assertTrue("No messages returned", messages != null && messages.size() > 0);+String message = messages.get(0);+assertEquals("INFO org.apache.logging.log4j.core." ++"PatternResolverDoesNotEvaluateThreadContextTest 123 This is a test", message);+}++@Test+public void testUserIsLookup() {+Logger logger = context.getLogger(getClass().getName());+ThreadContext.put(PARAMETER, "${java:version}");+try {+logger.info("This is a test");+} finally {+ThreadContext.remove(PARAMETER);+}+List<String> messages = listAppender.getMessages();+assertTrue("No messages returned", messages != null && messages.size() > 0);+String message = messages.get(0);+assertEquals("INFO org.apache.logging.log4j.core." ++"PatternResolverDoesNotEvaluateThreadContextTest ${java:version} This is a test", message);+}++@Test+public void testUserHasLookup() {+Logger logger = context.getLogger(getClass().getName());+ThreadContext.put(PARAMETER, "user${java:version}name");+try {+logger.info("This is a test");+} finally {+ThreadContext.remove(PARAMETER);+}+List<String> messages = listAppender.getMessages();+assertTrue("No messages returned",messages != null && messages.size() > 0);+String message = messages.get(0);+assertEquals("INFO org.apache.logging.log4j.core." ++"PatternResolverDoesNotEvaluateThreadContextTest user${java:version}name This is a test", message);+}+}
log4j-core/src/test/java/org/apache/logging/log4j/core/appender/routing/RoutingAppenderKeyLookupEvaluationTest.java+94 −0
@@ -0,0 +1,94 @@+/*+* Licensed to the Apache Software Foundation (ASF) under one or more+* contributor license agreements. See the NOTICE file distributed with+* this work for additional information regarding copyright ownership.+* The ASF licenses this file to You under the Apache license, Version 2.0+* (the "License"); you may not use this file except in compliance with+* the License. You may obtain a copy of the License at+*+* http://www.apache.org/licenses/LICENSE-2.0+*+* Unless required by applicable law or agreed to in writing, software+* distributed under the License is distributed on an "AS IS" BASIS,+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+* See the license for the specific language governing permissions and+* limitations under the license.+*/+package org.apache.logging.log4j.core.appender.routing;++import org.apache.logging.log4j.ThreadContext;+import org.apache.logging.log4j.core.Logger;+import org.apache.logging.log4j.junit.InitialLoggerContext;+import org.apache.logging.log4j.test.appender.ListAppender;+import org.junit.After;+import org.junit.Before;+import org.junit.Rule;+import org.junit.Test;++import static org.junit.Assert.assertEquals;+import static org.junit.Assert.assertTrue;++public class RoutingAppenderKeyLookupEvaluationTest {+private static final String CONFIG = "log4j-routing-lookup.xml";++private static final String KEY = "user";+private ListAppender app;++@Rule+public InitialLoggerContext init = new InitialLoggerContext(CONFIG);++@Before+public void setUp() throws Exception {+ThreadContext.remove(KEY);+this.app = this.init.getListAppender("List");+}++@After+public void tearDown() throws Exception {+this.app.clear();+this.init.getContext().stop();+ThreadContext.remove(KEY);+}++@Test+public void testRoutingNoUser() {+Logger logger = init.getLogger(getClass().getName());+logger.warn("no user");+String message = app.getMessages().get(0);+assertEquals("WARN ${ctx:user} no user", message);+}++@Test+public void testRoutingDoesNotMatchRoute() {+Logger logger = init.getLogger(getClass().getName());+ThreadContext.put(KEY, "noRouteExists");+logger.warn("unmatched user");+assertTrue(app.getMessages().isEmpty());+}++@Test+public void testRoutingContainsLookup() {+Logger logger = init.getLogger(getClass().getName());+ThreadContext.put(KEY, "${java:version}");+logger.warn("naughty user");+String message = app.getMessages().get(0);+assertEquals("WARN ${java:version} naughty user", message);+}++@Test+public void testRoutingMatchesEscapedLookup() {+Logger logger = init.getLogger(getClass().getName());+ThreadContext.put(KEY, "${upper:name}");+logger.warn("naughty user");+String message = app.getMessages().get(0);+assertEquals("WARN ${upper:name} naughty user", message);+}++@Test+public void testRoutesThemselvesNotEvaluated() {+Logger logger = init.getLogger(getClass().getName());+ThreadContext.put(KEY, "NAME");+logger.warn("unmatched user");+assertTrue(app.getMessages().isEmpty());+}+}
src/site/xdoc/manual/configuration.xml.vm+50 −1
@@ -1375,7 +1375,56 @@ public class AwesomeTest {could not successfully create a configuration (e.g. no log4j2.xml file was found).</td></tr>-+<tr>+<td><a name="enableJndiContextSelector"/>log4j2.enableJndiContextSelector</td>+<td>false</td>+<td>+When true, the Log4j context selector that uses the JNDI java protocol is enabled. When false, the default, they are disabled.+</td>+</tr>+<tr>+<td><a name="enableJndiJms"/>log4j2.enableJndiJms+</td>+<td>false</td>+<td>+When true, the Log4j JMS Appender that uses JNDI's java protocol is enabled. When false, the default, they are+disabled.+</td>+</tr>+<tr>+<td><a name="enableJndiLookup"/>log4j2.enableJndiLookup+</td>+<td>false</td>+<td>+When true, the Log4j lookup that uses JNDI's java protocol is enabled. When false, the default, they are disabled.+</td>+</tr>+<tr>+<td><a name="allowedLdapClasses"/>log4j2.allowedLdapClasses+</td>+<td> </td>+<td>+System property that specifies fully qualified class names that may be accessed by LDAP. The classes+must implement Serializable. By default only Java primitive classes are allowed.+</td>+</tr>+<tr>+<td><a name="allowedLdapHosts"/>log4j2.allowedLdapHosts+</td>+<td> </td>+<td>+System property that adds host names or ip addresses that may be access by LDAP. By default it only allows+the local host names and ip addresses.+</td>+</tr>+<tr>+<td><a name="allowedJndiProtocols"/>log4j2.allowedJndiProtocols+</td>+<td> </td>+<td>+System property that adds protocol names that JNDI will allow. By default it only allows java, ldap, and ldaps.+</td>+</tr><tr><td>disableThreadContext</td><td>false</td>log4j-api/pom.xml | 2 +-log4j-core/pom.xml | 4 ++--.../java/org/apache/logging/log4j/core/util/Constants.java | 1 -pom.xml | 2 +-4 files changed, 4 insertions(+), 5 deletions(-)
log4j-core/src/test/java/org/apache/logging/log4j/core/lookup/JndiDisabledLookupTest.java+63 −0
@@ -0,0 +1,63 @@+/*+* Licensed to the Apache Software Foundation (ASF) under one or more+* contributor license agreements. See the NOTICE file distributed with+* this work for additional information regarding copyright ownership.+* The ASF licenses this file to You under the Apache license, Version 2.0+* (the "License"); you may not use this file except in compliance with+* the License. You may obtain a copy of the License at+*+* http://www.apache.org/licenses/LICENSE-2.0+*+* Unless required by applicable law or agreed to in writing, software+* distributed under the License is distributed on an "AS IS" BASIS,+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+* See the license for the specific language governing permissions and+* limitations under the license.+*/+package org.apache.logging.log4j.core.lookup;++import javax.naming.Context;+import javax.naming.InitialContext;+import javax.naming.NamingException;++import org.junit.After;+import org.junit.AfterClass;+import org.junit.Before;+import org.junit.BeforeClass;+import org.junit.Test;+import org.mockejb.jndi.MockContextFactory;++import static org.junit.Assert.assertEquals;+import static org.junit.Assert.assertNull;++/**+* JndiLookupTest+*/+public class JndiDisabledLookupTest {++private static final String TEST_CONTEXT_RESOURCE_NAME = "logging/context-name";+private static final String TEST_CONTEXT_NAME = "app-1";++private Context context;++@Before+public void before() throws NamingException {+MockContextFactory.setAsInitial();+context = new InitialContext();+context.bind(JndiLookup.CONTAINER_JNDI_RESOURCE_PATH_PREFIX + TEST_CONTEXT_RESOURCE_NAME, TEST_CONTEXT_NAME);+}++@After+public void after() {+try {+context.close();+} catch (final NamingException ignored) {+}+MockContextFactory.revertSetAsInitial();+}++@Test(expected = IllegalStateException.class)+public void testLookup() {+final StrLookup lookup = new JndiLookup();+}+}
log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/StrSubstitutor.java+171 −54
@@ -25,6 +25,8 @@import java.util.Properties;import org.apache.logging.log4j.core.LogEvent;+import org.apache.logging.log4j.core.config.Configuration;+import org.apache.logging.log4j.status.StatusLogger;import org.apache.logging.log4j.util.Strings;/**@@ -142,21 +144,25 @@ public class StrSubstitutor {* Constant for the default escape character.*/public static final char DEFAULT_ESCAPE = '$';-+/*** Constant for the default variable prefix.*/public static final StrMatcher DEFAULT_PREFIX = StrMatcher.stringMatcher(DEFAULT_ESCAPE + "{");-+/*** Constant for the default variable suffix.*/public static final StrMatcher DEFAULT_SUFFIX = StrMatcher.stringMatcher("}");-+/*** Constant for the default value delimiter of a variable.*/-public static final StrMatcher DEFAULT_VALUE_DELIMITER = StrMatcher.stringMatcher(":-");+public static final String DEFAULT_VALUE_DELIMITER_STRING = ":-";+public static final StrMatcher DEFAULT_VALUE_DELIMITER = StrMatcher.stringMatcher(DEFAULT_VALUE_DELIMITER_STRING);++public static final String ESCAPE_DELIMITER_STRING = ":\\-";+public static final StrMatcher DEFAULT_VALUE_ESCAPE_DELIMITER = StrMatcher.stringMatcher(ESCAPE_DELIMITER_STRING);private static final int BUF_SIZE = 256;@@ -164,26 +170,44 @@ public class StrSubstitutor {* Stores the escape character.*/private char escapeChar;+/*** Stores the variable prefix.*/private StrMatcher prefixMatcher;+/*** Stores the variable suffix.*/private StrMatcher suffixMatcher;+/*** Stores the default variable value delimiter*/+private String valueDelimiterString;private StrMatcher valueDelimiterMatcher;++/**+* Escape string to avoid matching the value delimiter matcher;+*/+private StrMatcher valueEscapeDelimiterMatcher;+/*** Variable resolution is delegated to an implementer of VariableResolver.*/private StrLookup variableResolver;+/*** The flag whether substitution in variable names is enabled.*/-private boolean enableSubstitutionInVariables;+private boolean enableSubstitutionInVariables = false;++/**+* The currently active Configuration for use by ConfigurationAware StrLookup implementations.+*/+private Configuration configuration;++private boolean recursiveEvaluationAllowed;//-----------------------------------------------------------------------/**@@ -193,6 +217,7 @@ public class StrSubstitutor {public StrSubstitutor() {this(null, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);}+/*** Creates a new instance and initializes it. Uses defaults for variable* prefix and suffix and the escaping character.@@ -244,6 +269,16 @@ public StrSubstitutor(final Map<String, String> valueMap, final String prefix, fthis(new MapLookup(valueMap), prefix, suffix, escape, valueDelimiter);}+/**+* Creates a new instance and initializes it. Uses defaults for variable+* prefix and suffix and the escaping character.+*+* @param properties the map with the variables' values, may be null+*/+public StrSubstitutor(final Properties properties) {+this(toTypeSafeMap(properties));+}+/*** Creates a new instance and initializes it.*@@ -300,7 +335,9 @@ public StrSubstitutor(final StrLookup variableResolver, final String prefix, finpublic StrSubstitutor(final StrLookup variableResolver, final StrMatcher prefixMatcher,final StrMatcher suffixMatcher,final char escape) {-this(variableResolver, prefixMatcher, suffixMatcher, escape, DEFAULT_VALUE_DELIMITER);+this(variableResolver, prefixMatcher, suffixMatcher, escape, DEFAULT_VALUE_DELIMITER,+DEFAULT_VALUE_ESCAPE_DELIMITER);+this.valueDelimiterString = DEFAULT_VALUE_DELIMITER_STRING;}/**@@ -313,13 +350,48 @@ public StrSubstitutor(final StrLookup variableResolver, final StrMatcher prefixM* @param valueDelimiterMatcher the variable default value delimiter matcher, may be null* @throws IllegalArgumentException if the prefix or suffix is null*/-public StrSubstitutor(-final StrLookup variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher, final char escape, final StrMatcher valueDelimiterMatcher) {+public StrSubstitutor(final StrLookup variableResolver, final StrMatcher prefixMatcher,+final StrMatcher suffixMatcher, final char escape, final StrMatcher valueDelimiterMatcher) {+this.setVariableResolver(variableResolver);+this.setVariablePrefixMatcher(prefixMatcher);+this.setVariableSuffixMatcher(suffixMatcher);+this.setEscapeChar(escape);+this.setValueDelimiterMatcher(valueDelimiterMatcher);+}++/**+* Creates a new instance and initializes it.+*+* @param variableResolver the variable resolver, may be null+* @param prefixMatcher the prefix for variables, not null+* @param suffixMatcher the suffix for variables, not null+* @param escape the escape character+* @param valueDelimiterMatcher the variable default value delimiter matcher, may be null+* @param valueEscapeMatcher the matcher to escape defaulting, may be null.+* @throws IllegalArgumentException if the prefix or suffix is null+*/+public StrSubstitutor(final StrLookup variableResolver, final StrMatcher prefixMatcher,+final StrMatcher suffixMatcher, final char escape, final StrMatcher valueDelimiterMatcher,+final StrMatcher valueEscapeMatcher) {this.setVariableResolver(variableResolver);this.setVariablePrefixMatcher(prefixMatcher);this.setVariableSuffixMatcher(suffixMatcher);this.setEscapeChar(escape);this.setValueDelimiterMatcher(valueDelimiterMatcher);+valueEscapeDelimiterMatcher = valueEscapeMatcher;+}++StrSubstitutor(final StrSubstitutor other) {+this.setVariableResolver(other.getVariableResolver());+this.setVariablePrefixMatcher(other.getVariablePrefixMatcher());+this.setVariableSuffixMatcher(other.getVariableSuffixMatcher());+this.setEscapeChar(other.getEscapeChar());+this.setValueDelimiterMatcher(other.valueDelimiterMatcher);+this.valueEscapeDelimiterMatcher = other.valueEscapeDelimiterMatcher;+this.configuration = other.configuration;+this.recursiveEvaluationAllowed = other.isRecursiveEvaluationAllowed();+this.enableSubstitutionInVariables = other.isEnableSubstitutionInVariables();+this.valueDelimiterString = other.valueDelimiterString;}//-----------------------------------------------------------------------@@ -374,6 +446,19 @@ public static String replace(final Object source, final Properties valuePropertireturn StrSubstitutor.replace(source, valueMap);}+private static Map<String, String> toTypeSafeMap(final Properties properties) {+final Map<String, String> map = new HashMap<String, String>(properties.size());+for (final String name : properties.stringPropertyNames()) {+map.put(name, properties.getProperty(name));+}+return map;+}++private static String handleFailedReplacement(String input, Throwable throwable) {+StatusLogger.getLogger().error("Replacement failed on {}", input, throwable);+return input;+}+//-----------------------------------------------------------------------/*** Replaces all the occurrences of variables with their matching values@@ -399,8 +484,12 @@ public String replace(final LogEvent event, final String source) {return null;}final StringBuilder buf = new StringBuilder(source);-if (!substitute(event, buf, 0, source.length())) {-return source;+try {+if (!substitute(event, buf, 0, source.length())) {+return source;+}+} catch (Throwable t) {+return handleFailedReplacement(source, t);}return buf.toString();}@@ -441,8 +530,12 @@ public String replace(final LogEvent event, final String source, final int offsereturn null;}final StringBuilder buf = new StringBuilder(length).append(source, offset, length);-if (!substitute(event, buf, 0, length)) {-return source.substring(offset, offset + length);+try {+if (!substitute(event, buf, 0, length)) {+return source.substring(offset, offset + length);+}+} catch (Throwable t) {+return handleFailedReplacement(source, t);}return buf.toString();}@@ -475,7 +568,11 @@ public String replace(final LogEvent event, final char[] source) {return null;}final StringBuilder buf = new StringBuilder(source.length).append(source);-substitute(event, buf, 0, source.length);+try {+substitute(event, buf, 0, source.length);+} catch (Throwable t) {+return handleFailedReplacement(new String(source), t);+}return buf.toString();}@@ -517,7 +614,11 @@ public String replace(final LogEvent event, final char[] source, final int offsereturn null;}final StringBuilder buf = new StringBuilder(length).append(source, offset, length);-substitute(event, buf, 0, length);+try {+substitute(event, buf, 0, length);+} catch (Throwable t) {+return handleFailedReplacement(new String(source, offset, length), t);+}return buf.toString();}@@ -549,7 +650,11 @@ public String replace(final LogEvent event, final StringBuffer source) {return null;}final StringBuilder buf = new StringBuilder(source.length()).append(source);-substitute(event, buf, 0, buf.length());+try {+substitute(event, buf, 0, buf.length());+} catch (Throwable t) {+return handleFailedReplacement(source.toString(), t);+}return buf.toString();}@@ -591,7 +696,11 @@ public String replace(final LogEvent event, final StringBuffer source, final intreturn null;}final StringBuilder buf = new StringBuilder(length).append(source, offset, length);-substitute(event, buf, 0, length);+try {+substitute(event, buf, 0, length);+} catch (Throwable t) {+return handleFailedReplacement(source.substring(offset, offset + length), t);+}return buf.toString();}@@ -623,7 +732,11 @@ public String replace(final LogEvent event, final StringBuilder source) {return null;}final StringBuilder buf = new StringBuilder(source.length()).append(source);-substitute(event, buf, 0, buf.length());+try {+substitute(event, buf, 0, buf.length());+} catch (Throwable t) {+return handleFailedReplacement(source.toString(), t);+}return buf.toString();}/**@@ -664,7 +777,11 @@ public String replace(final LogEvent event, final StringBuilder source, final inreturn null;}final StringBuilder buf = new StringBuilder(length).append(source, offset, length);-substitute(event, buf, 0, length);+try {+substitute(event, buf, 0, length);+} catch (Throwable t) {+return handleFailedReplacement(source.substring(offset, offset + length), t);+}return buf.toString();}@@ -694,8 +811,13 @@ public String replace(final LogEvent event, final Object source) {if (source == null) {return null;}-final StringBuilder buf = new StringBuilder().append(source);-substitute(event, buf, 0, buf.length());+String stringValue = String.valueOf(source);+final StringBuilder buf = new StringBuilder(stringValue.length()).append(stringValue);+try {+substitute(event, buf, 0, buf.length());+} catch (Throwable t) {+return handleFailedReplacement(stringValue, t);+}return buf.toString();}@@ -753,7 +875,12 @@ public boolean replaceIn(final LogEvent event, final StringBuffer source, finalreturn false;}final StringBuilder buf = new StringBuilder(length).append(source, offset, length);-if (!substitute(event, buf, 0, length)) {+try {+if (!substitute(event, buf, 0, length)) {+return false;+}+} catch (Throwable t) {+StatusLogger.getLogger().error("Replacement failed on {}", source, t);return false;}source.replace(offset, offset + length, buf.toString());@@ -868,122 +995,133 @@ private int substitute(final LogEvent event, final StringBuilder buf, final intfinal StrMatcher valueDelimiterMatcher = getValueDelimiterMatcher();final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();-final boolean top = (priorVariables == null);+final boolean top = priorVariables == null;boolean altered = false;int lengthChange = 0;char[] chars = getChars(buf);int bufEnd = offset + length;int pos = offset;while (pos < bufEnd) {-final int startMatchLen = prefixMatcher.isMatch(chars, pos, offset,-bufEnd);+final int startMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd);if (startMatchLen == 0) {pos++;+} else // found variable start marker+if (pos > offset && chars[pos - 1] == escape) {+// escaped+buf.deleteCharAt(pos - 1);+chars = getChars(buf);+lengthChange--;+altered = true;+bufEnd--;} else {-// found variable start marker-if (pos > offset && chars[pos - 1] == escape) {-// escaped-buf.deleteCharAt(pos - 1);-chars = getChars(buf);-lengthChange--;-altered = true;-bufEnd--;-} else {-// find suffix-final int startPos = pos;-pos += startMatchLen;-int endMatchLen = 0;-int nestedVarCount = 0;-while (pos < bufEnd) {-if (substitutionInVariablesEnabled-&& (endMatchLen = prefixMatcher.isMatch(chars,-pos, offset, bufEnd)) != 0) {-// found a nested variable start-nestedVarCount++;-pos += endMatchLen;-continue;-}+// find suffix+final int startPos = pos;+pos += startMatchLen;+int endMatchLen = 0;+int nestedVarCount = 0;+while (pos < bufEnd) {+if (substitutionInVariablesEnabled+&& (endMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd)) != 0) {+// found a nested variable start+nestedVarCount++;+pos += endMatchLen;+continue;+}-endMatchLen = suffixMatcher.isMatch(chars, pos, offset,-bufEnd);-if (endMatchLen == 0) {-pos++;-} else {-// found variable end marker-if (nestedVarCount == 0) {… diff truncated
log4j-core/src/test/java/org/apache/logging/log4j/core/lookup/StrSubstitutorTest.java+135 −3
@@ -20,15 +20,13 @@import java.util.Map;import org.apache.logging.log4j.ThreadContext;+import org.apache.logging.log4j.core.LogEvent;import org.junit.AfterClass;import org.junit.BeforeClass;import org.junit.Test;import static org.junit.Assert.*;-/**-*-*/public class StrSubstitutorTest {private static final String TESTKEY = "TestKey";@@ -65,4 +63,138 @@ public void testLookup() {value = subst.replace("${BadKey:-Unknown}-${ctx:BadKey:-}-${sys:BadKey:-Unknown}");assertEquals("Unknown--Unknown", value);}++@Test+public void testDefault() {+final Map<String, String> map = new HashMap<String, String>();+map.put(TESTKEY, TESTVAL);+final StrLookup lookup = new Interpolator(new MapLookup(map));+final StrSubstitutor subst = new StrSubstitutor(lookup);+ThreadContext.put(TESTKEY, TESTVAL);+//String value = subst.replace("${sys:TestKey1:-${ctx:TestKey}}");+final String value = subst.replace("${sys:TestKey1:-${ctx:TestKey}}");+assertEquals("${ctx:TestKey}", value);+}++@Test+public void testDefaultReferencesLookupValue() {+final Map<String, String> map = new HashMap<String, String>();+map.put(TESTKEY, "${java:version}");+final StrLookup lookup = new Interpolator(new MapLookup(map));+final StrSubstitutor subst = new StrSubstitutor(lookup);+subst.setRecursiveEvaluationAllowed(false);+final String value = subst.replace("${sys:TestKey1:-${ctx:TestKey}}");+assertEquals("${ctx:TestKey}", value);+}++@Test+public void testInfiniteSubstitutionOnString() {+final StrLookup lookup = new Interpolator(new MapLookup(new HashMap<String, String>()));+final StrSubstitutor subst = new StrSubstitutor(lookup);+subst.setRecursiveEvaluationAllowed(true);+String infiniteSubstitution = "${${::-${::-$${::-j}}}}";+assertEquals(infiniteSubstitution, subst.replace(infiniteSubstitution));+}++@Test+public void testInfiniteSubstitutionOnStringBuilder() {+final StrLookup lookup = new Interpolator(new MapLookup(new HashMap<String, String>()));+final StrSubstitutor subst = new StrSubstitutor(lookup);+subst.setRecursiveEvaluationAllowed(true);+String infiniteSubstitution = "${${::-${::-$${::-j}}}}";+assertEquals(infiniteSubstitution, subst.replace(null, new StringBuilder(infiniteSubstitution)));+}++@Test+public void testRecursiveSubstitution() {+final Map<String, String> map = new HashMap<String, String>();+map.put("first", "${ctx:first}");+map.put("second", "secondValue");+final StrLookup lookup = new Interpolator(new MapLookup(map));+final StrSubstitutor subst = new StrSubstitutor(lookup);+subst.setRecursiveEvaluationAllowed(true);+assertEquals("${ctx:first} and secondValue", subst.replace("${ctx:first} and ${ctx:second}"));+}++@Test+public void testRecursiveWithDefault() {+final Map<String, String> map = new HashMap<String, String>();+map.put("first", "${ctx:first:-default}");+final StrLookup lookup = new Interpolator(new MapLookup(map));+final StrSubstitutor subst = new StrSubstitutor(lookup);+subst.setRecursiveEvaluationAllowed(true);+assertEquals("default", subst.replace("${ctx:first}"));+}++@Test+public void testRecursiveWithRecursiveDefault() {+final Map<String, String> map = new HashMap<String, String>();+map.put("first", "${ctx:first:-${ctx:first}}");+final StrLookup lookup = new Interpolator(new MapLookup(map));+final StrSubstitutor subst = new StrSubstitutor(lookup);+subst.setRecursiveEvaluationAllowed(true);+assertEquals("${ctx:first}", subst.replace("${ctx:first}"));+}++@Test+public void testNestedSelfReferenceWithRecursiveEvaluation() {+final Map<String, String> map = new HashMap<String, String>();+map.put("first", "${${ctx:first}}");+final StrLookup lookup = new Interpolator(new MapLookup(map));+final StrSubstitutor subst = new StrSubstitutor(lookup);+subst.setRecursiveEvaluationAllowed(true);+assertEquals("${${ctx:first}}}", subst.replace("${ctx:first}"));+}++@Test+public void testRandomWithRecursiveDefault() {+final Map<String, String> map = new HashMap<String, String>();+map.put("first", "${env:RANDOM:-${ctx:first}}");+final StrLookup lookup = new Interpolator(new MapLookup(map));+final StrSubstitutor subst = new StrSubstitutor(lookup);+subst.setRecursiveEvaluationAllowed(true);+assertEquals("${ctx:first}", subst.replace("${ctx:first}"));+}++@Test+public void testNoRecursiveEvaluationWithDefault() {+final Map<String, String> map = new HashMap<String, String>();+map.put("first", "${java:version}");+map.put("second", "${java:runtime}");+final StrLookup lookup = new Interpolator(new MapLookup(map));+final StrSubstitutor subst = new StrSubstitutor(lookup);+subst.setRecursiveEvaluationAllowed(false);+assertEquals("${java:version}}", subst.replace("${ctx:first:-${ctx:second}}"));+}++@Test+public void testNoRecursiveEvaluationWithDepthOne() {+final Map<String, String> map = new HashMap<String, String>();+map.put("first", "${java:version}");+final StrLookup lookup = new Interpolator(new MapLookup(map));+final StrSubstitutor subst = new StrSubstitutor(lookup);+subst.setRecursiveEvaluationAllowed(false);+assertEquals("${java:version}", subst.replace("${ctx:first}"));+}++@Test+public void testLookupThrows() {+final StrSubstitutor subst = new StrSubstitutor(new Interpolator(new StrLookup() {++@Override+public String lookup(String key) {+if ("throw".equals(key)) {+throw new RuntimeException();+}+return "success";+}++@Override+public String lookup(LogEvent event, String key) {+return lookup(key);+}+}));+subst.setRecursiveEvaluationAllowed(false);+assertEquals("success ${foo:throw} success", subst.replace("${foo:a} ${foo:throw} ${foo:c}"));+}}
log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ConfigurationStrSubstitutor.java+63 −0
@@ -0,0 +1,63 @@+/*+* Licensed to the Apache Software Foundation (ASF) under one or more+* contributor license agreements. See the NOTICE file distributed with+* this work for additional information regarding copyright ownership.+* The ASF licenses this file to You under the Apache license, Version 2.0+* (the "License"); you may not use this file except in compliance with+* the License. You may obtain a copy of the License at+*+* http://www.apache.org/licenses/LICENSE-2.0+*+* Unless required by applicable law or agreed to in writing, software+* distributed under the License is distributed on an "AS IS" BASIS,+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+* See the license for the specific language governing permissions and+* limitations under the license.+*/+package org.apache.logging.log4j.core.lookup;++import java.util.Map;+import java.util.Properties;++/**+* {@link RuntimeStrSubstitutor} is a {@link StrSubstitutor} which only supports recursive evaluation of lookups.+* This can be dangerous when combined with user-provided inputs, and should only be used on data directly from+* a configuration.+*/+public final class ConfigurationStrSubstitutor extends StrSubstitutor {++public ConfigurationStrSubstitutor() {+}++public ConfigurationStrSubstitutor(final Map<String, String> valueMap) {+super(valueMap);+}++public ConfigurationStrSubstitutor(final Properties properties) {+super(properties);+}++public ConfigurationStrSubstitutor(final StrLookup lookup) {+super(lookup);+}++public ConfigurationStrSubstitutor(final StrSubstitutor other) {+super(other);+}++@Override+boolean isRecursiveEvaluationAllowed() {+return true;+}++@Override+void setRecursiveEvaluationAllowed(final boolean recursiveEvaluationAllowed) {+throw new UnsupportedOperationException(+"recursiveEvaluationAllowed cannot be modified within ConfigurationStrSubstitutor");+}++@Override+public String toString() {+return "ConfigurationStrSubstitutor{" + super.toString() + "}";+}+}
log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java+14 −3
@@ -48,8 +48,10 @@import org.apache.logging.log4j.core.filter.AbstractFilterable;import org.apache.logging.log4j.core.impl.Log4jContextFactory;import org.apache.logging.log4j.core.layout.PatternLayout;+import org.apache.logging.log4j.core.lookup.ConfigurationStrSubstitutor;import org.apache.logging.log4j.core.lookup.Interpolator;import org.apache.logging.log4j.core.lookup.MapLookup;+import org.apache.logging.log4j.core.lookup.RuntimeStrSubstitutor;import org.apache.logging.log4j.core.lookup.StrLookup;import org.apache.logging.log4j.core.lookup.StrSubstitutor;import org.apache.logging.log4j.core.net.Advertiser;@@ -102,7 +104,8 @@ public abstract class AbstractConfiguration extends AbstractFilterable implementprivate List<CustomLevelConfig> customLevels = Collections.emptyList();private final ConcurrentMap<String, String> properties = new ConcurrentHashMap<String, String>();private final StrLookup tempLookup = new Interpolator(properties);-private final StrSubstitutor subst = new StrSubstitutor(tempLookup);+private final StrSubstitutor subst = new RuntimeStrSubstitutor(tempLookup);+private final StrSubstitutor configurationStrSubstitutor = new ConfigurationStrSubstitutor(subst);private LoggerConfig root = new LoggerConfig();private final ConcurrentMap<String, Object> componentMap = new ConcurrentHashMap<String, Object>();protected final List<String> pluginPackages = new ArrayList<String>();@@ -338,12 +341,16 @@ protected void doConfigure() {final Node first = rootNode.getChildren().get(0);createConfiguration(first, null);if (first.getObject() != null) {-subst.setVariableResolver((StrLookup) first.getObject());+StrLookup lookup = (StrLookup) first.getObject();+subst.setVariableResolver(lookup);+configurationStrSubstitutor.setVariableResolver(lookup);}} else {final Map<String, String> map = this.getComponent(CONTEXT_PROPERTIES);final StrLookup lookup = map == null ? null : new MapLookup(map);-subst.setVariableResolver(new Interpolator(lookup, pluginPackages));+Interpolator interpolator = new Interpolator(lookup, pluginPackages);+subst.setVariableResolver(interpolator);+configurationStrSubstitutor.setVariableResolver(interpolator);}boolean setLoggers = false;@@ -496,6 +503,10 @@ public StrSubstitutor getStrSubstitutor() {return subst;}+public StrSubstitutor getConfigurationStrSubstitutor() {+return configurationStrSubstitutor;+}+@Overridepublic void setConfigurationMonitor(final ConfigurationMonitor monitor) {this.monitor = monitor;
log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/Interpolator.java+13 −8
@@ -24,6 +24,7 @@import org.apache.logging.log4j.core.LogEvent;import org.apache.logging.log4j.core.config.plugins.util.PluginManager;import org.apache.logging.log4j.core.config.plugins.util.PluginType;+import org.apache.logging.log4j.core.net.JndiManager;import org.apache.logging.log4j.core.util.Loader;import org.apache.logging.log4j.core.util.ReflectionUtil;import org.apache.logging.log4j.status.StatusLogger;@@ -62,7 +63,10 @@ public Interpolator(final StrLookup defaultLookup, final List<String> pluginPackfor (final Map.Entry<String, PluginType<?>> entry : plugins.entrySet()) {try {final Class<? extends StrLookup> clazz = entry.getValue().getPluginClass().asSubclass(StrLookup.class);-lookups.put(entry.getKey(), ReflectionUtil.instantiate(clazz));+if (!clazz.getName().equals("org.apache.logging.log4j.core.lookup.JndiLookup")+|| JndiManager.isJndiLookupEnabled()) {+lookups.put(entry.getKey().toLowerCase(), ReflectionUtil.instantiate(clazz));+}} catch (final Exception ex) {LOGGER.error("Unable to create Lookup for {}", entry.getKey(), ex);}@@ -87,15 +91,16 @@ public Interpolator(final Map<String, String> properties) {lookups.put("main", MapLookup.MAIN_SINGLETON);lookups.put("java", new JavaLookup());// JNDI-try {-// [LOG4J2-703] We might be on Android-lookups.put("jndi",-Loader.newCheckedInstanceOf("org.apache.logging.log4j.core.lookup.JndiLookup", StrLookup.class));-} catch (final Throwable e) {-// java.lang.VerifyError: org/apache/logging/log4j/core/lookup/JndiLookup-LOGGER.warn(+if (JndiManager.isJndiLookupEnabled()) {+try {+// [LOG4J2-703] We might be on Android+lookups.put("jndi", Loader.newCheckedInstanceOf("org.apache.logging.log4j.core.lookup.JndiLookup", StrLookup.class));+} catch (final Throwable e) {+// java.lang.VerifyError: org/apache/logging/log4j/core/lookup/JndiLookup+LOGGER.warn("JNDI lookup class is not available because this JRE does not support JNDI. JNDI string lookups will not be available, continuing configuration.",e);+}}// JMX input argstry {
log4j-core/src/main/java/org/apache/logging/log4j/core/net/JndiManager.java+41 −1
@@ -17,6 +17,8 @@package org.apache.logging.log4j.core.net;+import java.net.URI;+import java.net.URISyntaxException;import java.util.Properties;import javax.naming.Context;@@ -26,6 +28,7 @@import org.apache.logging.log4j.core.appender.AbstractManager;import org.apache.logging.log4j.core.appender.ManagerFactory;import org.apache.logging.log4j.core.util.JndiCloser;+import org.apache.logging.log4j.util.PropertiesUtil;/*** JNDI {@link javax.naming.Context} manager.@@ -35,9 +38,31 @@public class JndiManager extends AbstractManager {private static final JndiManagerFactory FACTORY = new JndiManagerFactory();+private static final String PREFIX = "log4j2.enableJndi";+private static final String JAVA_SCHEME = "java";private final Context context;+private static boolean isJndiEnabled(final String subKey) {+return PropertiesUtil.getProperties().getBooleanProperty(PREFIX + subKey, false);+}++public static boolean isJndiEnabled() {+return isJndiContextSelectorEnabled() || isJndiJmsEnabled() || isJndiLookupEnabled();+}++public static boolean isJndiContextSelectorEnabled() {+return isJndiEnabled("ContextSelector");+}++public static boolean isJndiJmsEnabled() {+return isJndiEnabled("Jms");+}++public static boolean isJndiLookupEnabled() {+return isJndiEnabled("Lookup");+}+private JndiManager(final String name, final Context context) {super(name);this.context = context;@@ -125,13 +150,28 @@ protected void releaseSub() {*/@SuppressWarnings("unchecked")public <T> T lookup(final String name) throws NamingException {-return (T) this.context.lookup(name);+if (context == null) {+return null;+}+try {+URI uri = new URI(name);+if (uri.getScheme() == null || uri.getScheme().equals(JAVA_SCHEME)) {+return (T) this.context.lookup(name);+}+LOGGER.warn("Unsupported JNDI URI - {}", name);+} catch (URISyntaxException ex) {+LOGGER.warn("Invalid JNDI URI - {}", name);+}+return null;}private static class JndiManagerFactory implements ManagerFactory<JndiManager, Properties> {@Overridepublic JndiManager createManager(final String name, final Properties data) {+if (!isJndiEnabled()) {+throw new IllegalStateException(String.format("JNDI must be enabled by setting one of the %s* properties to true", PREFIX));+}try {return new JndiManager(name, new InitialContext(data));} catch (final NamingException e) {
src/changes/announcement.vm+10 −3
@@ -26,7 +26,14 @@ Log4j that provides significant improvements over its predecessor, Log4j 1.x, anmany other modern features such as support for Markers, property substitution using Lookups, and asynchronousLoggers. In addition, Log4j 2 will not lose events while reconfiguring.-This is the sixth GA release. It contains several bugfixes and new features.+The major changes contained in this release include:++* Address CVE-2021-45046 and CVE-2021-45105 by disabling recursive evaluation of Lookups during log event processing. Recursive evaluation is still allowed while generating the configuration.+* Adddress CVE-2021-44882 by removing processing of Lookups in the Message Pattern Converter of the Pattern Layout and+preventing JNDI operations to use any protocols other than java.+* The JndiLookup, JndiContextSelector, and JMSAppender now require individual system properties to be enabled.++The JNDI components are now disabled by default and may separately be enabled with three individual properties; log4j2.enableJndiContextSelector, log4j2.enableJndiJms, and log4j2.enableJndiLookup.## Hack to improve layout: replace all pairs of spaces with a single new-line$release.description.replaceAll(" ", "@@ -131,8 +138,8 @@ o#if($!issue != "") $issue. #end ${action} #if($!dueto != "")Thanks to $dueto. ### End of main loop#end-Apache Log4j ${relVersion} requires a minimum of Java 6 to build and run. This will be the last release of Log4j 2 to-support Java 6. Future releases will require a minimum of Java 7.+Apache Log4j ${relVersion} requires a minimum of Java 6 to build and run. It is not expected that any future Java 6+releases will be provided.Basic compatibility with Log4j 1.x is provided through the log4j-1.2-api component, however it does not implement some of thevery implementation specific classes and methods. The package names and Maven groupId have been changed to
log4j-distribution/pom.xml+50 −0
@@ -310,6 +310,56 @@</execution> --></executions></plugin>+<!-- calculate checksums of source release for Apache dist area -->+<plugin>+<groupId>net.nicoulaj.maven.plugins</groupId>+<artifactId>checksum-maven-plugin</artifactId>+<version>1.11</version>+<executions>+<execution>+<id>calculate-checksums</id>+<goals>+<goal>files</goal>+</goals>+<!-- execute prior to maven-gpg-plugin:sign due to https://github.com/nicoulaj/checksum-maven-plugin/issues/112 -->+<phase>post-integration-test</phase>+<configuration>+<algorithms>+<algorithm>SHA-256</algorithm>+<algorithm>SHA-512</algorithm>+</algorithms>+<!-- https://maven.apache.org/apache-resource-bundles/#source-release-assembly-descriptor -->+<fileSets>+<fileSet>+<directory>${project.build.directory}</directory>+<includes>+<include>apache-log4j-${project.version}-src.zip</include>+<include>apache-log4j-${project.version}-src.tar.gz</include>+<include>apache-log4j-${project.version}-bin.zip</include>+<include>apache-log4j-${project.version}-bin.tar.gz</include>+</includes>+</fileSet>+</fileSets>+<csvSummary>false</csvSummary>+</configuration>+</execution>+</executions>+</plugin>+<plugin>+<groupId>org.apache.maven.plugins</groupId>+<artifactId>maven-gpg-plugin</artifactId>+<executions>+<execution>+<id>sign-release-artifacts</id>+<goals>+<goal>sign</goal>+</goals>+<configuration>+<keyname>${Log4jSigningUserName}</keyname>+</configuration>+</execution>+</executions>+</plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-site-plugin</artifactId>
src/site/xdoc/manual/configuration.xml.vm+0 −26
@@ -1399,32 +1399,6 @@ public class AwesomeTest {When true, the Log4j lookup that uses JNDI's java protocol is enabled. When false, the default, they are disabled.</td></tr>-<tr>-<td><a name="allowedLdapClasses"/>log4j2.allowedLdapClasses-</td>-<td> </td>-<td>-System property that specifies fully qualified class names that may be accessed by LDAP. The classes-must implement Serializable. By default only Java primitive classes are allowed.-</td>-</tr>-<tr>-<td><a name="allowedLdapHosts"/>log4j2.allowedLdapHosts-</td>-<td> </td>-<td>-System property that adds host names or ip addresses that may be access by LDAP. By default it only allows-the local host names and ip addresses.-</td>-</tr>-<tr>-<td><a name="allowedJndiProtocols"/>log4j2.allowedJndiProtocols-</td>-<td> </td>-<td>-System property that adds protocol names that JNDI will allow. By default it only allows java, ldap, and ldaps.-</td>-</tr><tr><td>disableThreadContext</td><td>false</td>
References
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2021-45046
- WEBhttps://www.oracle.com/security-alerts/cpujul2022.html
- WEBhttps://www.oracle.com/security-alerts/cpujan2022.html
- WEBhttps://www.oracle.com/security-alerts/cpuapr2022.html
- WEBhttps://www.oracle.com/security-alerts/alert-cve-2021-44228.html
- WEBhttps://www.openwall.com/lists/oss-security/2021/12/18/1
- WEBhttps://www.openwall.com/lists/oss-security/2021/12/15/3
- WEBhttps://www.openwall.com/lists/oss-security/2021/12/14/4
- WEBhttps://www.kb.cert.org/vuls/id/930724
- WEBhttps://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00646.html
- WEBhttps://www.debian.org/security/2021/dsa-5022
- WEBhttps://www.cve.org/CVERecord?id=CVE-2021-44228