Security context
Critical· 9.0GHSA-7rjr-3q55-vv33 CVE-2021-45046CWE-502CWE-917Published Dec 14, 2021

Incomplete fix for Apache Log4j vulnerability

Research this 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

2.13.0 → fixed in 2.16.02.4.0 → fixed in 2.12.21.8.0 → fixed in 1.9.21.10.0 → fixed in 1.10.81.11.0 → fixed in 1.11.112.0.0 → fixed in 2.0.120 → fixed in 2.3.1

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

rgoers· Dec 20, 2021, 05:43 AM+1231−224a0b0e11497
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.java
create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/RuntimeStrSubstitutor.java
create mode 100644 log4j-core/src/test/java/org/apache/logging/log4j/core/PatternResolverDoesNotEvaluateThreadContextTest.java
create mode 100644 log4j-core/src/test/java/org/apache/logging/log4j/core/appender/routing/RoutingAppenderKeyLookupEvaluationTest.java
create mode 100644 log4j-core/src/test/resources/log4j-routing-lookup.xml
create 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 to
Log4j that provides significant improvements over its predecessor, Log4j 1.x, and provides
many other modern features such as support for Markers, property substitution using Lookups, and asynchronous
Loggers. 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.1
Changes 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 the
very 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>&nbsp;</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>&nbsp;</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>&nbsp;</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, f
this(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, fin
public 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 valueProperti
return 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 offse
return 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 offse
return 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 int
return 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 in
return 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, final
return 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 int
final 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 implement
private 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;
+ }
+
@Override
public 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> pluginPack
for (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 args
try {
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> {
@Override
public 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, an
many other modern features such as support for Markers, property substitution using Lookups, and asynchronous
Loggers. 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 the
very 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>&nbsp;</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>&nbsp;</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>&nbsp;</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>
More files changed — see the full commit.

References