This is an automated email from the ASF dual-hosted git repository.
vy pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
The following commit(s) were added to refs/heads/2.x by this push:
new 11e74d78f6 Merge changes in version `2.25.4` back to `2.x` (#4085)
11e74d78f6 is described below
commit 11e74d78f64c205569023bcc20ba95dacaeb5fda
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Sun Mar 29 22:04:26 2026 +0200
Merge changes in version `2.25.4` back to `2.x` (#4085)
---
.github/workflows/build.yaml | 1 +
.../apache/log4j/layout/Log4j1XmlLayoutTest.java | 42 ++-
.../logging/log4j/message/MapMessageTest.java | 23 +-
.../logging/log4j/util/StringBuildersTest.java | 41 ++-
.../apache/logging/log4j/message/MapMessage.java | 10 +-
.../apache/logging/log4j/util/StringBuilders.java | 55 +++-
log4j-core-test/pom.xml | 17 +
.../logging/log4j/core/test/package-info.java | 2 +-
.../log4j/core/appender/LineReadingTcpServer.java | 18 +-
.../log4j/core/appender/TlsSocketAppenderTest.java | 346 +++++++++++++++++++++
.../log4j/core/appender/X509Certificates.java | 193 ++++++++++++
.../log4j/core/layout/Rfc5424LayoutTest.java | 202 +++++++++++-
.../log4j/core/layout/XmlLayoutJUnit5Test.java | 111 +++++++
.../log4j/core/net/ssl/SslConfigurationTest.java | 21 ++
.../logging/log4j/core/util/TransformTest.java | 90 ++++++
.../resources/TlsSocketAppenderTest/log4j2.xml | 42 +++
log4j-core/pom.xml | 7 +-
.../log4j/core/jackson/Log4jXmlObjectMapper.java | 175 ++++++++++-
.../logging/log4j/core/layout/Rfc5424Layout.java | 251 ++++++++++++++-
.../log4j/core/net/ssl/SslConfiguration.java | 10 +-
.../apache/logging/log4j/core/util/Transform.java | 137 +++++---
.../layout/template/json/util/JsonWriterTest.java | 18 ++
.../layout/template/json/util/JsonWriter.java | 14 +-
log4j-parent/pom.xml | 7 +
pom.xml | 10 +-
.../update_com_fasterxml_jackson_jackson_bom.xml | 4 +-
src/changelog/2.25.4/.release-notes.adoc.ftl | 32 ++
src/changelog/2.25.4/.release.xml | 21 ++
...3975_prevent_warning_for_last_null_argument.xml | 13 +
src/changelog/2.25.4/4022_rfc5424-param-names.xml | 13 +
.../2.25.4/4033_fix_custom_throwable_to_sting.xml | 13 +
src/changelog/2.25.4/4060_resource-loading.xml | 13 +
src/changelog/2.25.4/4061_ssl-connection.xml | 14 +
src/changelog/2.25.4/4073_rfc5424-sd-param.xml | 12 +
.../2.25.4/4077_xml-control-characters.xml | 12 +
.../2.25.4/4078_log4j1-xml-control-characters.xml | 12 +
.../2.25.4/4079_map-message-control-characters.xml | 12 +
src/changelog/2.25.4/4080_jtl-nan.xml | 12 +
38 files changed, 1919 insertions(+), 107 deletions(-)
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 42d44a60ce..df49f55229 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -21,6 +21,7 @@ on:
push:
branches:
- "2.x"
+ - "2.25.x"
- "release/2*"
pull_request:
diff --git
a/log4j-1.2-api/src/test/java/org/apache/log4j/layout/Log4j1XmlLayoutTest.java
b/log4j-1.2-api/src/test/java/org/apache/log4j/layout/Log4j1XmlLayoutTest.java
index 1af12167a6..9c8953f2d0 100644
---
a/log4j-1.2-api/src/test/java/org/apache/log4j/layout/Log4j1XmlLayoutTest.java
+++
b/log4j-1.2-api/src/test/java/org/apache/log4j/layout/Log4j1XmlLayoutTest.java
@@ -72,7 +72,8 @@ class Log4j1XmlLayoutTest {
final String expected = "<log4j:event logger=\"a.B\" timestamp=\"" +
event.getTimeMillis()
+ "\" level=\"INFO\" thread=\"main\">\r\n"
+ "<log4j:message><![CDATA[Hello, World]]></log4j:message>\r\n"
- + "<log4j:locationInfo class=\"pack.MyClass\"
method=\"myMethod\" file=\"MyClass.java\" line=\"17\"/>\r\n"
+ + "<log4j:locationInfo class=\"pack.MyClass\"
method=\"myMethod\" file=\"MyClass.java\""
+ + " line=\"17\"/>\r\n"
+ "<log4j:properties>\r\n"
+ "<log4j:data name=\"key1\" value=\"value1\"/>\r\n"
+ "<log4j:data name=\"key2\" value=\"value2\"/>\r\n"
@@ -81,4 +82,43 @@ class Log4j1XmlLayoutTest {
assertEquals(expected, result);
}
+
+ @Test
+ void testWithInvalidXmlCharacters() {
+ final Log4j1XmlLayout layout = Log4j1XmlLayout.createLayout(true,
true);
+
+ final String message =
"<>'\"&A\uD800B\uDE00C\u0000\u0001\u0002\u0003\uFFFE\uFFFF";
+ final String expectedMessage =
"<>'\"&A\uFFFDB\uFFFDC\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD";
+ final String expectedEscapedMessage =
+
"<>'"&A\uFFFDB\uFFFDC\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD";
+
+ final StringMap contextMap = ContextDataFactory.createContextData(1);
+ contextMap.putValue(message, message);
+ final Log4jLogEvent event = Log4jLogEvent.newBuilder()
+ .setLoggerName(message)
+ .setLevel(Level.forName(message, 100))
+ .setMessage(new SimpleMessage(message))
+ .setTimeMillis(System.currentTimeMillis() + 17)
+ .setIncludeLocation(true)
+ .setSource(new StackTraceElement(message, message, message,
17))
+ .setContextData(contextMap)
+ .build();
+
+ final String result = layout.toSerializable(event);
+
+ final String expected =
+ "<log4j:event logger=\"" + expectedEscapedMessage + "\"
timestamp=\"" + event.getTimeMillis()
+ + "\" level=\"" + expectedEscapedMessage + "\"
thread=\"main\">\r\n"
+ + "<log4j:message><![CDATA[" + expectedMessage +
"]]></log4j:message>\r\n"
+ + "<log4j:locationInfo class=\"" +
expectedEscapedMessage
+ + "\" method=\"" + expectedEscapedMessage
+ + "\" file=\"" + expectedEscapedMessage + "\"
line=\"17\"/>\r\n"
+ + "<log4j:properties>\r\n"
+ + "<log4j:data name=\"" + expectedEscapedMessage + "\"
value=\"" + expectedEscapedMessage
+ + "\"/>\r\n"
+ + "</log4j:properties>\r\n"
+ + "</log4j:event>\r\n\r\n";
+
+ assertEquals(expected, result);
+ }
}
diff --git
a/log4j-api-test/src/test/java/org/apache/logging/log4j/message/MapMessageTest.java
b/log4j-api-test/src/test/java/org/apache/logging/log4j/message/MapMessageTest.java
index a36bb6b4ae..db29a18949 100644
---
a/log4j-api-test/src/test/java/org/apache/logging/log4j/message/MapMessageTest.java
+++
b/log4j-api-test/src/test/java/org/apache/logging/log4j/message/MapMessageTest.java
@@ -16,6 +16,7 @@
*/
package org.apache.logging.log4j.message;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -72,12 +73,26 @@ class MapMessageTest {
@Test
void testXMLEscape() {
- final String testMsg = "Test message <foo>";
+ final String notBmp = new String(Character.toChars(0x10000));
+ final String invalid = "A\uD800B\uDE00C\0\1\2\3";
+ final String expectedInvalid =
"A\uFFFDB\uFFFDC\uFFFD\uFFFD\uFFFD\uFFFD";
+ final String key = "k<e&y> '\"\t\r\n" + notBmp + invalid;
+ final String value = "v>al<u& '\"\t\r\n" + notBmp + invalid;
final StringMapMessage msg = new StringMapMessage();
- msg.put("message", testMsg);
+ msg.put(key, value);
final String result = msg.getFormattedMessage(new String[] {"XML"});
- final String expected = "<Map>\n <Entry key=\"message\">Test message
<foo></Entry>\n" + "</Map>";
- assertEquals(expected, result);
+
+ assertThat(result)
+ .isEqualTo(
+ "<Map>\n" //
+ + " <Entry key=\"k<e&y>
'"\t\r\n"
+ + notBmp
+ + expectedInvalid
+ + "\">v>al<u& '"\t\r\n"
+ + notBmp
+ + expectedInvalid
+ + "</Entry>\n" //
+ + "</Map>");
}
@Test
diff --git
a/log4j-api-test/src/test/java/org/apache/logging/log4j/util/StringBuildersTest.java
b/log4j-api-test/src/test/java/org/apache/logging/log4j/util/StringBuildersTest.java
index 8722dd33e4..9c0a5caa61 100644
---
a/log4j-api-test/src/test/java/org/apache/logging/log4j/util/StringBuildersTest.java
+++
b/log4j-api-test/src/test/java/org/apache/logging/log4j/util/StringBuildersTest.java
@@ -16,10 +16,15 @@
*/
package org.apache.logging.log4j.util;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
/**
* Tests the StringBuilders class.
@@ -79,15 +84,37 @@ class StringBuildersTest {
assertEquals(jsonValueEscaped, sb.toString());
}
- @Test
- void escapeXMLCharactersCorrectly() {
- final String xmlValueNotEscaped = "<\"Salt&Peppa'\">";
- final String xmlValueEscaped =
"<"Salt&Peppa'">";
+ static Stream<Arguments> escapeXmlCharactersCorrectly() {
+ final char replacement = '\uFFFD';
+ return Stream.of(
+ // Empty
+ Arguments.of("", ""),
+ // characters that need to be escaped
+ Arguments.of("<\"Salt&Peppa'\">",
"<"Salt&Peppa'">"),
+ // control character replaced with U+FFFD
+ Arguments.of("A" + (char) 0x01 + "B", "A" + replacement + "B"),
+ // standalone low surrogate replaced with U+FFFD
+ Arguments.of("low" + Character.MIN_LOW_SURROGATE +
"surrogate", "low" + replacement + "surrogate"),
+ Arguments.of(Character.MIN_LOW_SURROGATE + "low", replacement
+ "low"),
+ // standalone high surrogate replaced with U+FFFD
+ Arguments.of("high" + Character.MIN_HIGH_SURROGATE +
"surrogate", "high" + replacement + "surrogate"),
+ Arguments.of(Character.MIN_HIGH_SURROGATE + "high",
replacement + "high"),
+ // FFFE and FFFF
+ Arguments.of("invalid\uFFFEchars", "invalid" + replacement +
"chars"),
+ Arguments.of("invalid\uFFFFchars", "invalid" + replacement +
"chars"),
+ // whitespace characters are preserved
+ Arguments.of("tab\tnewline\ncr\r", "tab\tnewline\ncr\r"),
+ // character beyond BMP (emoji) preserved as surrogate pair
+ Arguments.of("emoji " + "\uD83D\uDE00" + " end", "emoji " +
"\uD83D\uDE00" + " end"));
+ }
+ @ParameterizedTest
+ @MethodSource
+ void escapeXmlCharactersCorrectly(final String input, final String
expected) {
final StringBuilder sb = new StringBuilder();
- sb.append(xmlValueNotEscaped);
- assertEquals(xmlValueNotEscaped, sb.toString());
+ sb.append(input);
+ assertThat(sb.toString()).isEqualTo(input);
StringBuilders.escapeXml(sb, 0);
- assertEquals(xmlValueEscaped, sb.toString());
+ assertThat(sb.toString()).isEqualTo(expected);
}
}
diff --git
a/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessage.java
b/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessage.java
index f1144cee62..c78d59a0b5 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessage.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessage.java
@@ -358,10 +358,14 @@ public class MapMessage<M extends MapMessage<M, V>, V>
implements MultiFormatStr
public void asXml(final StringBuilder sb) {
sb.append("<Map>\n");
for (int i = 0; i < data.size(); i++) {
- sb.append(" <Entry
key=\"").append(data.getKeyAt(i)).append("\">");
- final int size = sb.length();
+ sb.append(" <Entry key=\"");
+ int start = sb.length();
+ sb.append(data.getKeyAt(i));
+ StringBuilders.escapeXml(sb, start);
+ sb.append("\">");
+ start = sb.length();
ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb);
- StringBuilders.escapeXml(sb, size);
+ StringBuilders.escapeXml(sb, start);
sb.append("</Entry>\n");
}
sb.append("</Map>");
diff --git
a/log4j-api/src/main/java/org/apache/logging/log4j/util/StringBuilders.java
b/log4j-api/src/main/java/org/apache/logging/log4j/util/StringBuilders.java
index 8f697d53f1..c835e17eeb 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/StringBuilders.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/StringBuilders.java
@@ -27,6 +27,8 @@ import java.util.Map.Entry;
@InternalApi
public final class StringBuilders {
+ private static final char REPLACEMENT_CHAR = '\uFFFD';
+
private static final Class<?> timeClass;
private static final Class<?> dateClass;
@@ -310,8 +312,8 @@ public final class StringBuilders {
*/
public static void escapeXml(final StringBuilder toAppendTo, final int
start) {
int escapeCount = 0;
- for (int i = start; i < toAppendTo.length(); i++) {
- final char c = toAppendTo.charAt(i);
+ for (int i = start; i < toAppendTo.length(); ) {
+ final int c = toAppendTo.codePointAt(i);
switch (c) {
case '&':
escapeCount += 4;
@@ -323,15 +325,36 @@ public final class StringBuilders {
case '"':
case '\'':
escapeCount += 5;
+ break;
+ default:
+ // All invalid XML 1.0 characters have the same length as
the replacement character
+ // Therefore no additional adjustment is needed
}
+ i += Character.charCount(c);
}
final int lastChar = toAppendTo.length() - 1;
+ if (lastChar < 0) {
+ return;
+ }
toAppendTo.setLength(toAppendTo.length() + escapeCount);
int lastPos = toAppendTo.length() - 1;
- for (int i = lastChar; lastPos > i; i--) {
+ for (int i = lastChar; lastPos >= start; i--) {
final char c = toAppendTo.charAt(i);
+ // Handle surrogate pairs and invalid low surrogates
+ if (i > 0 && Character.isLowSurrogate(c)) {
+ final char previous = toAppendTo.charAt(i - 1);
+ // Invalid low surrogate
+ if (!Character.isHighSurrogate(previous)) {
+ toAppendTo.setCharAt(lastPos--, REPLACEMENT_CHAR);
+ } else {
+ toAppendTo.setCharAt(lastPos--, c);
+ toAppendTo.setCharAt(lastPos--, previous);
+ i--;
+ }
+ continue;
+ }
switch (c) {
case '&':
toAppendTo.setCharAt(lastPos--, ';');
@@ -369,8 +392,32 @@ public final class StringBuilders {
toAppendTo.setCharAt(lastPos--, '&');
break;
default:
- toAppendTo.setCharAt(lastPos--, c);
+ toAppendTo.setCharAt(lastPos--, isValidXml10(c) ? c :
REPLACEMENT_CHAR);
}
}
}
+
+ /**
+ * Checks if a BMP {@code char} is a valid XML 1.0 character.
+ *
+ * <p>This method is restricted to characters in the BMP, i.e. represented
by one UTF-16 code unit.</p>
+ *
+ * @param ch a BMP {@code char} to validate
+ * @return {@code true} if it is a valid XML 1.0 character
+ */
+ private static boolean isValidXml10(final char ch) {
+ // XML 1.0 valid characters (Fifth Edition):
+ // #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] |
[#x10000-#x10FFFF]
+
+ // [#x20–#xD7FF] (placed early as a fast path for the most common case)
+ return (ch >= ' ' && ch < Character.MIN_SURROGATE)
+ // #x9
+ || ch == '\t'
+ // #xA
+ || ch == '\n'
+ // #xD
+ || ch == '\r'
+ // [#xE000-#xFFFD]
+ || (ch > Character.MAX_SURROGATE && ch <= 0xFFFD);
+ }
}
diff --git a/log4j-core-test/pom.xml b/log4j-core-test/pom.xml
index 8c8cbc19bb..34ccfc0453 100644
--- a/log4j-core-test/pom.xml
+++ b/log4j-core-test/pom.xml
@@ -64,8 +64,19 @@
<!-- Additional version of LMAX Disruptor to test -->
<disruptor4.version>4.0.0</disruptor4.version>
<json-unit.version>2.40.1</json-unit.version>
+ <bouncycastle.version>1.83</bouncycastle.version>
</properties>
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcpkix-jdk18on</artifactId>
+ <version>${bouncycastle.version}</version>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
<dependencies>
<dependency>
@@ -149,6 +160,12 @@
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcpkix-jdk18on</artifactId>
+ <scope>test</scope>
+ </dependency>
+
<!-- Other -->
<dependency>
<groupId>commons-codec</groupId>
diff --git
a/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/package-info.java
b/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/package-info.java
index 70fbbc2b6f..8e58650f6f 100644
---
a/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/package-info.java
+++
b/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/package-info.java
@@ -15,7 +15,7 @@
* limitations under the license.
*/
@Export
-@Version("2.26.0")
+@Version("2.25.3")
@BaselineIgnore("2.25.0")
package org.apache.logging.log4j.core.test;
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/LineReadingTcpServer.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/LineReadingTcpServer.java
index 9f4028423a..df923a457c 100644
---
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/LineReadingTcpServer.java
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/LineReadingTcpServer.java
@@ -58,6 +58,8 @@ final class LineReadingTcpServer implements AutoCloseable {
private volatile boolean running;
+ private InetAddress bindAddress = InetAddress.getLoopbackAddress();
+
private ServerSocket serverSocket;
private Socket clientSocket;
@@ -74,6 +76,11 @@ final class LineReadingTcpServer implements AutoCloseable {
this.serverSocketFactory = serverSocketFactory;
}
+ // For testing purposes
+ void setBindAddress(final InetAddress bindAddress) {
+ this.bindAddress = bindAddress;
+ }
+
synchronized void start(final String name, final int port) throws
IOException {
if (!running) {
running = true;
@@ -83,8 +90,7 @@ final class LineReadingTcpServer implements AutoCloseable {
}
private ServerSocket createServerSocket(final int port) throws IOException
{
- final ServerSocket serverSocket =
- serverSocketFactory.createServerSocket(port, 1,
InetAddress.getLoopbackAddress());
+ final ServerSocket serverSocket =
serverSocketFactory.createServerSocket(port, 1, bindAddress);
serverSocket.setReuseAddress(true);
serverSocket.setSoTimeout(0); // Zero indicates `accept()` will block
indefinitely
await("server socket binding")
@@ -104,12 +110,12 @@ final class LineReadingTcpServer implements AutoCloseable
{
}
private void acceptClients() {
- try {
- while (running) {
+ while (running) {
+ try {
acceptClient();
+ } catch (final Exception error) {
+ LOGGER.error("failed accepting client connections", error);
}
- } catch (final Exception error) {
- LOGGER.error("failed accepting client connections", error);
}
}
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/TlsSocketAppenderTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/TlsSocketAppenderTest.java
new file mode 100644
index 0000000000..171f1849ab
--- /dev/null
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/TlsSocketAppenderTest.java
@@ -0,0 +1,346 @@
+/*
+ * 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;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.stream.Stream;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLServerSocket;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.net.SslSocketManager;
+import org.apache.logging.log4j.test.TestProperties;
+import org.apache.logging.log4j.test.junit.UsingTestProperties;
+import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+@UsingTestProperties
+class TlsSocketAppenderTest {
+
+ // Test DNS names and IP addresses
+ private static final String TARGET_HOSTNAME = "log4j.localhost";
+ private static final String TARGET_IP = "::1";
+ private static final String ATTACKER_HOSTNAME = "not-log4j.localhost";
+ private static final String ATTACKER_IP = "127.0.0.1";
+
+ // Test PKI material
+ private static final KeyPair CA_KEY_PAIR =
X509Certificates.generateKeyPair();
+ private static final KeyPair SERVER_KEY_PAIR =
X509Certificates.generateKeyPair();
+ private static final KeyPair CLIENT_KEY_PAIR =
X509Certificates.generateKeyPair();
+
+ private static final X509Certificate CA_CERT;
+
+ private static final X509Certificate TARGET_CERT1;
+ private static final X509Certificate TARGET_CERT2;
+ private static final X509Certificate TARGET_CERT3;
+
+ private static final X509Certificate ATTACKER_CERT1;
+ private static final X509Certificate ATTACKER_CERT2;
+ private static final X509Certificate ATTACKER_CERT3;
+
+ /** Client certificate used for mutual TLS (mTLS) scenarios. */
+ private static final X509Certificate CLIENT_CERT;
+
+ static {
+ try {
+ CA_CERT = X509Certificates.generateCACertificate(CA_KEY_PAIR);
+ PrivateKey caPrivateKey = CA_KEY_PAIR.getPrivate();
+
+ // Certificates with CN only
+ TARGET_CERT1 = X509Certificates.generateServerCertificate(
+ SERVER_KEY_PAIR, caPrivateKey, "CN=" + TARGET_HOSTNAME,
null, null);
+ ATTACKER_CERT1 = X509Certificates.generateServerCertificate(
+ SERVER_KEY_PAIR, caPrivateKey, "CN=" + ATTACKER_HOSTNAME,
null, null);
+
+ // Certificates with SAN (DNS)
+ TARGET_CERT2 = X509Certificates.generateServerCertificate(
+ SERVER_KEY_PAIR, caPrivateKey, "CN=Test Server",
TARGET_HOSTNAME, null);
+ ATTACKER_CERT2 = X509Certificates.generateServerCertificate(
+ SERVER_KEY_PAIR, caPrivateKey, "CN=Test Attacker Server",
ATTACKER_HOSTNAME, null);
+
+ // Certificates with SAN (IP)
+ TARGET_CERT3 = X509Certificates.generateServerCertificate(
+ SERVER_KEY_PAIR, caPrivateKey, "CN=Test Server", null,
TARGET_IP);
+ ATTACKER_CERT3 = X509Certificates.generateServerCertificate(
+ SERVER_KEY_PAIR, caPrivateKey, "CN=Test Attacker Server",
null, ATTACKER_IP);
+
+ CLIENT_CERT =
X509Certificates.generateClientCertificate(CLIENT_KEY_PAIR, caPrivateKey,
"CN=Test Client");
+
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ // Store parameters
+
+ private static final String KEYSTORE_TYPE = "PKCS12";
+ private static final char[] KEYSTORE_PWD = "aKeyStoreSecret".toCharArray();
+ private static final String TRUSTSTORE_TYPE = "PKCS12";
+ private static final char[] TRUSTSTORE_PWD =
"aTrustStoreSecret".toCharArray();
+
+ @TempDir
+ private static Path certPath;
+
+ @BeforeAll
+ static void setup() {
+ Assumptions.assumeTrue(() -> {
+ // RFC 6761 recommends that *.localhost resolve to the loopback
interface, but DNS behavior varies
+ // across platforms and test environments. If two distinct
hostnames do not resolve to the local
+ // machine, tests that require different hostnames cannot be
executed reliably.
+ try {
+ InetAddress.getByName(TARGET_HOSTNAME);
+ InetAddress.getByName(ATTACKER_HOSTNAME);
+ return true;
+ } catch (UnknownHostException e) {
+ return false;
+ }
+ });
+ }
+
+ static Stream<Arguments>
connectionAlwaysSucceedsWithoutHostnameVerification() {
+ return Stream.of(
+ Arguments.of(TARGET_HOSTNAME, ATTACKER_CERT1),
+ Arguments.of(TARGET_HOSTNAME, ATTACKER_CERT2),
+ Arguments.of(TARGET_IP, ATTACKER_CERT3));
+ }
+
+ static Stream<Arguments> connectionSucceedsOnHostNameMatch() {
+ return Stream.of(
+ // No client certificate
+ Arguments.of(TARGET_HOSTNAME, TARGET_CERT1, null),
+ Arguments.of(TARGET_HOSTNAME, TARGET_CERT2, null),
+ Arguments.of(TARGET_IP, TARGET_CERT3, null),
+
+ // These tests ensure that connections to the attacher fail
because of hostname mismatch,
+ // not because of other TLS issues.
+ Arguments.of(ATTACKER_HOSTNAME, ATTACKER_CERT1, null),
+ Arguments.of(ATTACKER_HOSTNAME, ATTACKER_CERT2, null),
+ Arguments.of(ATTACKER_IP, ATTACKER_CERT3, null),
+
+ // Mutual TLS
+ Arguments.of(TARGET_HOSTNAME, TARGET_CERT1, CLIENT_CERT),
+ Arguments.of(TARGET_HOSTNAME, TARGET_CERT2, CLIENT_CERT),
+ Arguments.of(TARGET_IP, TARGET_CERT3, CLIENT_CERT));
+ }
+
+ static Stream<Arguments> connectionFailsOnHostNameMismatch() {
+ return Stream.of(
+ Arguments.of(TARGET_HOSTNAME, ATTACKER_CERT1),
+ Arguments.of(TARGET_HOSTNAME, ATTACKER_CERT2),
+ Arguments.of(TARGET_IP, ATTACKER_CERT3));
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void connectionAlwaysSucceedsWithoutHostnameVerification(
+ String hostName, X509Certificate serverCertificate, TestProperties
props) throws Exception {
+
+ TestTlsMaterial tls = createTlsMaterial(hostName, serverCertificate,
null);
+ applyClientTlsProperties(props, tls);
+ props.setProperty("ssl.verifyHostname", "false");
+
+ try (LineReadingTcpServer server = createTlsServer(hostName,
tls.serverSslContext, false)) {
+ props.setProperty("server.host", hostName);
+ props.setProperty("server.port",
server.getServerSocket().getLocalPort());
+
+ try (LoggerContext ctx = createLoggerContext()) {
+ Logger logger = ctx.getLogger(TlsSocketAppenderTest.class);
+
+ String expected = "Test message for host " + hostName;
+ logger.info(expected);
+
+ assertThat(server.pollLines(1)).containsExactly(expected);
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void connectionSucceedsOnHostNameMatch(
+ String hostName,
+ X509Certificate serverCertificate,
+ @Nullable X509Certificate clientCertificate,
+ TestProperties props)
+ throws Exception {
+
+ TestTlsMaterial tls = createTlsMaterial(hostName, serverCertificate,
clientCertificate);
+ applyClientTlsProperties(props, tls);
+
+ try (LineReadingTcpServer server = createTlsServer(hostName,
tls.serverSslContext, clientCertificate != null)) {
+ props.setProperty("server.host", hostName);
+ props.setProperty("server.port",
server.getServerSocket().getLocalPort());
+
+ try (LoggerContext ctx = createLoggerContext()) {
+ Logger logger = ctx.getLogger(TlsSocketAppenderTest.class);
+
+ String expected = "Test message for host " + hostName;
+ logger.info(expected);
+
+ assertThat(server.pollLines(1)).containsExactly(expected);
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void connectionFailsOnHostNameMismatch(String hostName, X509Certificate
serverCertificate, TestProperties props)
+ throws Exception {
+
+ // No mTLS needed; we only care about hostname verification failure.
+ TestTlsMaterial tls = createTlsMaterial(hostName, serverCertificate,
null);
+ applyClientTlsProperties(props, tls);
+
+ try (LineReadingTcpServer server = createTlsServer(hostName,
tls.serverSslContext, false)) {
+ props.setProperty("server.host", hostName);
+ props.setProperty("server.port",
server.getServerSocket().getLocalPort());
+
+ try (LoggerContext ctx = createLoggerContext()) {
+ assertSocketAppenderNotConnected(ctx, hostName);
+ }
+ }
+ }
+
+ private static TestTlsMaterial createTlsMaterial(
+ String hostName, X509Certificate serverCertificate, @Nullable
X509Certificate clientCertificate)
+ throws Exception {
+
+ // Client keystore: only populated when we test mutual TLS.
+ String clientKeystore = generateKeystore(
+ hostName + "-client", clientCertificate, clientCertificate !=
null ? CLIENT_KEY_PAIR : null);
+
+ String serverKeystore = generateKeystore(hostName + "-server",
serverCertificate, SERVER_KEY_PAIR);
+
+ String truststore = generateTruststore(hostName);
+
+ SSLContext serverSslContext = SslContexts.createSslContext(
+ KEYSTORE_TYPE, serverKeystore, KEYSTORE_PWD, TRUSTSTORE_TYPE,
truststore, TRUSTSTORE_PWD);
+
+ return new TestTlsMaterial(clientKeystore, truststore,
serverSslContext);
+ }
+
+ private static void applyClientTlsProperties(TestProperties props,
TestTlsMaterial tls) {
+ props.setProperty("keystore.location", tls.clientKeystoreLocation);
+ props.setProperty("keystore.password", new String(KEYSTORE_PWD));
+ props.setProperty("keystore.type", KEYSTORE_TYPE);
+
+ props.setProperty("truststore.location", tls.truststoreLocation);
+ props.setProperty("truststore.password", new String(TRUSTSTORE_PWD));
+ props.setProperty("truststore.type", TRUSTSTORE_TYPE);
+ }
+
+ private static LineReadingTcpServer createTlsServer(String hostName,
SSLContext sslContext, boolean needClientAuth)
+ throws Exception {
+
+ LineReadingTcpServer server = new
LineReadingTcpServer(sslContext.getServerSocketFactory());
+
+ // Bind to all interfaces to allow testing with different host names.
+ server.setBindAddress(null);
+
+ server.start("TlsSocketAppenderTest-" + hostName, 0);
+
+ SSLServerSocket socket = (SSLServerSocket) server.getServerSocket();
+ socket.setNeedClientAuth(needClientAuth);
+
+ return server;
+ }
+
+ private static LoggerContext createLoggerContext() throws Exception {
+ URL configLocation =
TlsSocketAppenderTest.class.getResource("/TlsSocketAppenderTest/log4j2.xml");
+ assertThat(configLocation).isNotNull();
+
+ LoggerContext ctx = new LoggerContext("TlsSocketAppenderTest", null,
configLocation.toURI());
+ ctx.start();
+ return ctx;
+ }
+
+ private static void assertSocketAppenderNotConnected(LoggerContext ctx,
String hostName) {
+ SocketAppender appender = ctx.getConfiguration().getAppender("SOCKET-"
+ hostName);
+ assertThat(appender).isNotNull();
+ assertThat(appender.getManager()).isInstanceOf(SslSocketManager.class);
+
+ SslSocketManager manager = (SslSocketManager) appender.getManager();
+ Socket socket = manager.getSocket();
+
+ if (socket != null) {
+ assertThat(socket.isConnected()).isFalse();
+ }
+ }
+
+ private static String generateTruststore(String alias) throws Exception {
+ KeyStore trustStore = KeyStore.getInstance(TRUSTSTORE_TYPE);
+ trustStore.load(null, null);
+ trustStore.setCertificateEntry(alias, CA_CERT);
+
+ Path file = certPath.resolve(sanitizePath(alias) + "-truststore.p12");
+ try (OutputStream out = Files.newOutputStream(file)) {
+ trustStore.store(out, TRUSTSTORE_PWD);
+ }
+ return file.toAbsolutePath().toString();
+ }
+
+ private static String generateKeystore(
+ String alias, @Nullable X509Certificate certificate, @Nullable
KeyPair keyPair) throws Exception {
+
+ KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);
+ keyStore.load(null, null);
+
+ if (certificate != null && keyPair != null) {
+ keyStore.setKeyEntry(
+ alias, keyPair.getPrivate(), KEYSTORE_PWD, new
X509Certificate[] {certificate, CA_CERT});
+ }
+
+ Path file = certPath.resolve(sanitizePath(alias) + "-keystore.p12");
+ try (OutputStream out = Files.newOutputStream(file)) {
+ keyStore.store(out, KEYSTORE_PWD);
+ }
+ return file.toAbsolutePath().toString();
+ }
+
+ private static String sanitizePath(String alias) {
+ return alias.replace(':', '_');
+ }
+
+ private static class TestTlsMaterial {
+
+ private final @Nullable String clientKeystoreLocation;
+ private final String truststoreLocation;
+ private final SSLContext serverSslContext;
+
+ private TestTlsMaterial(String clientKeystoreLocation, String
truststoreLocation, SSLContext serverSslContext) {
+ this.clientKeystoreLocation = clientKeystoreLocation;
+ this.truststoreLocation = truststoreLocation;
+ this.serverSslContext = serverSslContext;
+ }
+ }
+}
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/X509Certificates.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/X509Certificates.java
new file mode 100644
index 0000000000..9b9caf2613
--- /dev/null
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/X509Certificates.java
@@ -0,0 +1,193 @@
+/*
+ * 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;
+
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Random;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.asn1.x509.KeyPurposeId;
+import org.bouncycastle.asn1.x509.KeyUsage;
+import org.bouncycastle.cert.CertIOException;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Utility class to generate X.509 certificates for testing purposes.
+ */
+final class X509Certificates {
+
+ private static final String CA_DN = "CN=Test CA";
+ private static final long MINUTE_IN_MILLIS = 60_000L;
+ private static final long YEAR_IN_MILLIS = 365L * 24 * 60 *
MINUTE_IN_MILLIS;
+
+ private static final KeyPairGenerator RSA_GENERATOR;
+ private static final Random RANDOM = new Random();
+
+ static {
+ try {
+ RSA_GENERATOR = KeyPairGenerator.getInstance("RSA");
+ RSA_GENERATOR.initialize(2048);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static KeyPair generateKeyPair() {
+ return RSA_GENERATOR.generateKeyPair();
+ }
+
+ static X509Certificate generateCACertificate(KeyPair keyPair) throws
Exception {
+ JcaX509v3CertificateBuilder builder =
getCertificateBuilder(keyPair.getPublic(), CA_DN, true);
+ addKeyUsageExtension(builder, KeyUsage.keyCertSign);
+ return buildCertificate(builder, keyPair.getPrivate());
+ }
+
+ /**
+ * Create and sign a server X.509 certificate for tests.
+ *
+ * <p>The produced certificate complies with {@code
sun.security.validator.EndEntityChecker}.</p>
+ *
+ * @param keyPair the subject key pair
+ * @param caKey the private key of the issuing CA used to sign the
certificate
+ * @param subjectDn the subject distinguished name for the certificate
(for example {@code CN=example.com})
+ * @param dnsAltSubject optional DNS Subject Alternative Name; pass {@code
null} to omit
+ * @param ipAltSubject optional IP Subject Alternative Name; pass {@code
null} to omit
+ * @return a signed X.509 server certificate
+ * @throws Exception if certificate creation or signing fails
+ */
+ static X509Certificate generateServerCertificate(
+ KeyPair keyPair,
+ PrivateKey caKey,
+ String subjectDn,
+ @Nullable String dnsAltSubject,
+ @Nullable String ipAltSubject)
+ throws Exception {
+ JcaX509v3CertificateBuilder builder =
getCertificateBuilder(keyPair.getPublic(), subjectDn, false);
+ // The required key usage for the server certificate depends on the
key exchange algorithm:
+ // - keyEncipherment for RSA key exchange (deprecated)
+ // - digitalSignature for ephemeral Diffie-Hellman key exchange (DHE
or ECDHE)
+ // - keyAgreement for static Diffie-Hellman key exchange (DH or ECDH)
+ addKeyUsageExtension(builder, KeyUsage.digitalSignature |
KeyUsage.keyAgreement);
+ addExtendedKeyUsageExtension(builder, KeyPurposeId.id_kp_serverAuth);
+ addSubjectAlternativeName(builder, dnsAltSubject, ipAltSubject);
+ return buildCertificate(builder, caKey);
+ }
+
+ /**
+ * Create and sign a client X.509 certificate for tests.
+ *
+ * <p>The produced certificate complies with {@code
sun.security.validator.EndEntityChecker}.</p>
+ *
+ * @param keyPair the subject key pair
+ * @param caKey the private key of the issuing CA used to sign the
certificate
+ * @param subjectDn the subject distinguished name for the certificate
(for example {@code CN=example.com})
+ * @return a signed X.509 server certificate
+ * @throws Exception if certificate creation or signing fails
+ */
+ static X509Certificate generateClientCertificate(KeyPair keyPair,
PrivateKey caKey, String subjectDn)
+ throws Exception {
+ JcaX509v3CertificateBuilder builder =
getCertificateBuilder(keyPair.getPublic(), subjectDn, false);
+ // The required key usage for the client certificate
+ addKeyUsageExtension(builder, KeyUsage.digitalSignature);
+ addExtendedKeyUsageExtension(builder, KeyPurposeId.id_kp_clientAuth);
+ return buildCertificate(builder, caKey);
+ }
+
+ private static JcaX509v3CertificateBuilder getCertificateBuilder(
+ PublicKey subjectPub, String subjectDn, boolean isCa) throws
CertIOException {
+ long now = System.currentTimeMillis();
+ Date notBefore = new Date(now - MINUTE_IN_MILLIS);
+ Date notAfter = new Date(now + YEAR_IN_MILLIS);
+ BigInteger serial = BigInteger.valueOf(RANDOM.nextLong()).abs();
+
+ X500Name issuer = new X500Name(CA_DN);
+ X500Name subject = new X500Name(subjectDn);
+
+ JcaX509v3CertificateBuilder builder =
+ new JcaX509v3CertificateBuilder(issuer, serial, notBefore,
notAfter, subject, subjectPub);
+
+ // Basic Constraints
+ builder.addExtension(Extension.basicConstraints, true, new
BasicConstraints(isCa));
+
+ return builder;
+ }
+
+ private static void addKeyUsageExtension(JcaX509v3CertificateBuilder
builder, int keyUsage) throws CertIOException {
+ builder.addExtension(Extension.keyUsage, true, new KeyUsage(keyUsage));
+ }
+
+ private static void
addExtendedKeyUsageExtension(JcaX509v3CertificateBuilder builder, KeyPurposeId
kp)
+ throws CertIOException {
+ builder.addExtension(Extension.extendedKeyUsage, false, new
ExtendedKeyUsage(kp));
+ }
+
+ private static GeneralName getIpAddressGeneralName(String ipAltSubject) {
+ return new GeneralName(GeneralName.iPAddress, ipAltSubject);
+ }
+
+ private static GeneralName getDnsGeneralName(String dnsAltSubject) {
+ return new GeneralName(GeneralName.dNSName, dnsAltSubject);
+ }
+
+ private static void addSubjectAlternativeName(
+ JcaX509v3CertificateBuilder builder, @Nullable String
dnsAltSubject, @Nullable String ipAltSubject)
+ throws CertIOException {
+ if (ipAltSubject != null || dnsAltSubject != null) {
+ List<GeneralName> names = new ArrayList<>();
+ if (dnsAltSubject != null) {
+ names.add(getDnsGeneralName(dnsAltSubject));
+ }
+ if (ipAltSubject != null) {
+ names.add(getIpAddressGeneralName(ipAltSubject));
+ }
+ GeneralName[] gna = names.toArray(new GeneralName[0]);
+ builder.addExtension(Extension.subjectAlternativeName, false, new
GeneralNames(gna));
+ }
+ }
+
+ private static X509Certificate
buildCertificate(JcaX509v3CertificateBuilder builder, PrivateKey signerKey)
+ throws OperatorCreationException, CertificateException {
+ ContentSigner signer = new
JcaContentSignerBuilder("SHA256withRSA").build(signerKey);
+
+ X509CertificateHolder holder = builder.build(signer);
+
+ return new JcaX509CertificateConverter().getCertificate(holder);
+ }
+
+ private X509Certificates() {
+ // private constructor to prevent instantiation
+ }
+}
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/layout/Rfc5424LayoutTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/layout/Rfc5424LayoutTest.java
index 87157ec68b..d4371b90dc 100644
---
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/layout/Rfc5424LayoutTest.java
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/layout/Rfc5424LayoutTest.java
@@ -27,14 +27,21 @@ import static org.junit.jupiter.api.Assertions.fail;
import java.net.InetAddress;
import java.net.UnknownHostException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Stream;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.MarkerManager;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.ConfigurationFactory;
@@ -42,21 +49,28 @@ import
org.apache.logging.log4j.core.config.DefaultConfiguration;
import org.apache.logging.log4j.core.config.Node;
import org.apache.logging.log4j.core.config.plugins.util.PluginBuilder;
import org.apache.logging.log4j.core.config.plugins.util.PluginManager;
+import org.apache.logging.log4j.core.impl.ContextDataFactory;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
import org.apache.logging.log4j.core.net.Facility;
import org.apache.logging.log4j.core.test.BasicConfigurationFactory;
import org.apache.logging.log4j.core.test.appender.ListAppender;
+import org.apache.logging.log4j.core.time.MutableInstant;
import org.apache.logging.log4j.core.util.Integers;
import org.apache.logging.log4j.core.util.KeyValuePair;
+import org.apache.logging.log4j.message.SimpleMessage;
import org.apache.logging.log4j.message.StructuredDataCollectionMessage;
import org.apache.logging.log4j.message.StructuredDataMessage;
import org.apache.logging.log4j.status.StatusLogger;
import org.apache.logging.log4j.test.junit.UsingAnyThreadContext;
import org.apache.logging.log4j.util.ProcessIdUtil;
+import org.apache.logging.log4j.util.StringMap;
import org.apache.logging.log4j.util.Strings;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
@UsingAnyThreadContext
@@ -76,11 +90,13 @@ class Rfc5424LayoutTest {
+ "[RequestContext@3692 ipAddress=\"192.168.0.120\"
loginId=\"JohnDoe\"] Transfer Complete",
PROCESSID);
private static final String lineEscaped3 = String.format(
- "ATM %s - [RequestContext@3692 escaped=\"Testing escaping #012
\\\" \\] \\\"\" loginId=\"JohnDoe\"] filled mdc",
+ "ATM %s - [RequestContext@3692 escaped=\"Testing escaping #012
\\\" \\] \\\"\" loginId=\"JohnDoe\"] filled"
+ + " mdc",
PROCESSID);
private static final String lineEscaped4 = String.format(
- "ATM %s Audit [Transfer@18060 Amount=\"200.00\"
FromAccount=\"123457\" ToAccount=\"123456\"]"
- + "[RequestContext@3692 escaped=\"Testing escaping #012
\\\" \\] \\\"\" ipAddress=\"192.168.0.120\" loginId=\"JohnDoe\"] Transfer
Complete",
+ "ATM %s Audit [Transfer@18060 Amount=\"200.00\"
FromAccount=\"123457\""
+ + " ToAccount=\"123456\"][RequestContext@3692
escaped=\"Testing escaping #012 \\\" \\] \\\"\""
+ + " ipAddress=\"192.168.0.120\" loginId=\"JohnDoe\"]
Transfer Complete",
PROCESSID);
private static final String collectionLine1 =
"[Transfer@18060 Amount=\"200.00\" FromAccount=\"123457\" " +
"ToAccount=\"123456\"]";
@@ -89,18 +105,29 @@ class Rfc5424LayoutTest {
"[RequestContext@3692 ipAddress=\"192.168.0.120\"
loginId=\"JohnDoe\"]";
private static final String collectionEndOfLine = "Transfer Complete";
+ private static final String NEW_LINE_ESCAPE = "\\n";
+ private static final String INCLUDED_KEYS = "key1, key2, locale";
+ private static final String EXCLUDED_KEYS = "key3, key4";
+
static ConfigurationFactory cf = new BasicConfigurationFactory();
+ private static PluginManager pluginManager;
+
@BeforeAll
static void setupClass() {
StatusLogger.getLogger().setLevel(Level.OFF);
ConfigurationFactory.setConfigurationFactory(cf);
final LoggerContext ctx = LoggerContext.getContext();
ctx.reconfigure();
+
+ pluginManager = new PluginManager(Node.CATEGORY);
+ pluginManager.collectPlugins();
}
@AfterAll
static void cleanupClass() {
+ pluginManager = null;
+
ConfigurationFactory.removeConfigurationFactory(cf);
}
@@ -749,9 +776,7 @@ class Rfc5424LayoutTest {
final Rfc5424Layout layout = new
Rfc5424Layout.Rfc5424LayoutBuilder().build();
checkDefaultValues(layout);
- final PluginManager manager = new PluginManager(Node.CATEGORY);
- manager.collectPlugins();
- final Object obj = new
PluginBuilder(manager.getPluginType("Rfc5424Layout"))
+ final Object obj = new
PluginBuilder(pluginManager.getPluginType("Rfc5424Layout"))
.withConfigurationNode(new Node())
.withConfiguration(new DefaultConfiguration())
.build();
@@ -792,4 +817,169 @@ class Rfc5424LayoutTest {
final Rfc5424Layout layout = Rfc5424Layout.newBuilder().build();
assertThat(layout.getLocalHostName()).isEqualTo(fqdn);
}
+
+ private static Map<String, String> attributeMap(String... keyValuePairs) {
+ Map<String, String> result = new HashMap<>();
+ for (int i = 0; i < keyValuePairs.length; i += 2) {
+ result.put(keyValuePairs[i], keyValuePairs[i + 1]);
+ }
+ return result;
+ }
+
+ private static Rfc5424Layout buildRfc5424Layout(Map<String, String>
attributes) {
+ Node node = new Node();
+ node.getAttributes().putAll(attributes);
+
+ Object object = new
PluginBuilder(pluginManager.getPluginType("Rfc5424Layout"))
+ .withConfigurationNode(node)
+ .withConfiguration(new DefaultConfiguration())
+ .build();
+
+ assertThat(object).isInstanceOf(Rfc5424Layout.class);
+ return (Rfc5424Layout) object;
+ }
+
+ private static Stream<Arguments>
testAcceptsDocumentedAttributesAndCompatibilityAliases() {
+ return Stream.of(
+ Arguments.of(
+ "documented attributes",
+ attributeMap(
+ "newLine",
+ "true",
+ "newLineEscape",
+ NEW_LINE_ESCAPE,
+ "useTlsMessageFormat",
+ "true",
+ "mdcRequired",
+ INCLUDED_KEYS)),
+ Arguments.of(
+ "compatibility aliases",
+ attributeMap(
+ "includeNL",
+ "true",
+ "escapeNL",
+ NEW_LINE_ESCAPE,
+ "useTLSMessageFormat",
+ "true",
+ "required",
+ INCLUDED_KEYS)));
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void testAcceptsDocumentedAttributesAndCompatibilityAliases(
+ String ignoredDisplayName, Map<String, String> attributes) {
+
+ Rfc5424Layout layout = buildRfc5424Layout(attributes);
+
+ assertThat(layout.isIncludeNewLine()).isTrue();
+ // The field contains Matcher.quote() escaped value, so we expect the
backslash to be escaped.
+ assertThat(layout.getEscapeNewLine()).isEqualTo("\\\\n");
+ assertThat(layout.isUseTlsMessageFormat()).isTrue();
+ assertThat(layout.getMdcRequired()).containsExactly("key1", "key2",
"locale");
+ }
+
+ private static Stream<Arguments>
testAcceptsIncludeAttributesAndCompatibilityAliases() {
+ return Stream.of(
+ Arguments.of("documented attributes",
attributeMap("mdcIncludes", INCLUDED_KEYS)),
+ Arguments.of("compatibility aliases", attributeMap("includes",
INCLUDED_KEYS)));
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void testAcceptsIncludeAttributesAndCompatibilityAliases(
+ String ignoredDisplayName, Map<String, String> attributes) {
+
+ Rfc5424Layout layout = buildRfc5424Layout(attributes);
+
+ assertThat(layout.getMdcIncludes()).containsExactly("key1", "key2",
"locale");
+ assertThat(layout.getMdcExcludes()).isNullOrEmpty();
+ }
+
+ private static Stream<Arguments>
testAcceptsExcludeAttributesAndCompatibilityAliases() {
+ return Stream.of(
+ Arguments.of("documented attributes",
attributeMap("mdcExcludes", EXCLUDED_KEYS)),
+ Arguments.of("compatibility aliases", attributeMap("excludes",
EXCLUDED_KEYS)));
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void testAcceptsExcludeAttributesAndCompatibilityAliases(
+ String ignoredDisplayName, Map<String, String> attributes) {
+
+ Rfc5424Layout layout = buildRfc5424Layout(attributes);
+
+ assertThat(layout.getMdcExcludes()).containsExactly("key3", "key4");
+ assertThat(layout.getMdcIncludes()).isNullOrEmpty();
+ }
+
+ private static Stream<Arguments> testRejectsIncludesAndExcludesTogether() {
+ return Stream.of(
+ Arguments.of(
+ "documented attributes",
+ attributeMap("mdcIncludes", INCLUDED_KEYS,
"mdcExcludes", EXCLUDED_KEYS)),
+ Arguments.of(
+ "compatibility aliases", attributeMap("includes",
INCLUDED_KEYS, "excludes", EXCLUDED_KEYS)));
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void testRejectsIncludesAndExcludesTogether(String ignoredDisplayName,
Map<String, String> attributes) {
+
+ Rfc5424Layout layout = buildRfc5424Layout(attributes);
+
+ // If both includes and excludes are specified, the layout will ignore
the includes and log an error about the
+ // invalid configuration.
+ assertThat(layout.getMdcExcludes()).containsExactly("key3", "key4");
+ assertThat(layout.getMdcIncludes()).isNullOrEmpty();
+ }
+
+ private static LogEvent createLogEventWithMdcParamName(final String
paramName) {
+ final MutableInstant instant = new MutableInstant();
+ instant.initFromEpochMilli(1L, 0);
+
+ final StringMap contextData = ContextDataFactory.createContextData();
+ contextData.putValue(paramName, "");
+
+ return Log4jLogEvent.newBuilder()
+ .setInstant(instant)
+ .setMessage(new SimpleMessage("MSG"))
+ .setContextData(contextData)
+ .build();
+ }
+
+ private static Stream<Arguments> testParamNameSanitization() {
+ return Stream.of(
+ Arguments.of("validName", "[mdc@32473 validName=\"\"]"),
+ Arguments.of("user name", "[mdc@32473 user?name=\"\"]"),
+ Arguments.of("user=name", "[mdc@32473 user?name=\"\"]"),
+ Arguments.of("user]name", "[mdc@32473 user?name=\"\"]"),
+ Arguments.of("user\"name", "[mdc@32473 user?name=\"\"]"),
+ Arguments.of("", "[mdc@32473 ?=\"\"]"),
+ Arguments.of(
+ "0123456789012345678901234567890123456789",
+ "[mdc@32473 01234567890123456789012345678901=\"\"]"));
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void testParamNameSanitization(final String paramName, final String
expectedStructuredData) {
+ final Rfc5424Layout layout = Rfc5424Layout.newBuilder().build();
+
+ final String actual =
layout.toSerializable(createLogEventWithMdcParamName(paramName));
+
+ final String expected = formatExpectedMessage(layout,
expectedStructuredData);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ private static String formatExpectedMessage(final Rfc5424Layout layout,
final String expectedStructuredData) {
+
+ final String timestamp = DateTimeFormatter.ISO_OFFSET_DATE_TIME
+ .withZone(ZoneId.systemDefault())
+ .format(Instant.ofEpochMilli(1L));
+
+ return String.format(
+ "<128>1 %s %s - %s - %s MSG", timestamp,
layout.getLocalHostName(), PROCESSID, expectedStructuredData);
+ }
}
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/layout/XmlLayoutJUnit5Test.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/layout/XmlLayoutJUnit5Test.java
new file mode 100644
index 0000000000..3dcf896c37
--- /dev/null
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/layout/XmlLayoutJUnit5Test.java
@@ -0,0 +1,111 @@
+/*
+ * 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.layout;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.Marker;
+import org.apache.logging.log4j.MarkerManager;
+import org.apache.logging.log4j.core.impl.ContextDataFactory;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.apache.logging.log4j.message.SimpleMessage;
+import org.apache.logging.log4j.spi.DefaultThreadContextStack;
+import org.apache.logging.log4j.util.StringMap;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class XmlLayoutJUnit5Test {
+
+ private static Log4jLogEvent createLogEventWithString(final String str) {
+ final Marker marker = MarkerManager.getMarker("marker" + str);
+
+ final RuntimeException thrown = new RuntimeException("thrown" + str);
+ thrown.addSuppressed(new IllegalStateException("suppressed" + str));
+
+ final StringMap contextData = ContextDataFactory.createContextData();
+ contextData.putValue("mdcKey" + str, "mdcValue" + str);
+
+ final DefaultThreadContextStack contextStack = new
DefaultThreadContextStack();
+ contextStack.clear();
+ contextStack.push("contextStack" + str);
+
+ final StackTraceElement source =
+ new StackTraceElement("class" + str, "method" + str, "file" +
str + ".java", 123);
+
+ return Log4jLogEvent.newBuilder()
+ .setLoggerName("logger" + str)
+ .setMarker(marker)
+ .setLoggerFqcn("fqcn" + str)
+ .setLevel(Level.DEBUG)
+ .setMessage(new SimpleMessage("message" + str))
+ .setThrown(thrown)
+ .setContextData(contextData)
+ .setContextStack(contextStack)
+ .setThreadName("thread" + str)
+ .setSource(source)
+ .setTimeMillis(1L)
+ .build();
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "\u0000",
+ "\u001F",
+ // hi surrogate
+ "\uD800",
+ // low surrogate
+ "\uDC00",
+ // invalid chars
+ "\uFFFE",
+ "\uFFFF"
+ })
+ void testInvalidXmlCharsAreSanitized(final String invalidXmlChars) {
+ final Log4jLogEvent event = createLogEventWithString(invalidXmlChars);
+ final AbstractJacksonLayout layout = XmlLayout.newBuilder()
+ .setCompact(true)
+ .setIncludeStacktrace(true)
+ .setLocationInfo(true)
+ .setProperties(true)
+ .build();
+ final String str = layout.toSerializable(event);
+ assertThat(str).doesNotContain(invalidXmlChars).contains("\uFFFD");
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ " ",
+ "A",
+ // First character from supplementary plane
+ "\uD801\uDC00",
+ // Last character from supplementary plane
+ "\uDBFF\uDFFF"
+ })
+ void testValidXmlCharsAreKept(final String validXmlChars) {
+ final Log4jLogEvent event = createLogEventWithString(validXmlChars);
+ final AbstractJacksonLayout layout = XmlLayout.newBuilder()
+ .setCompact(true)
+ .setIncludeStacktrace(true)
+ .setLocationInfo(true)
+ .setProperties(true)
+ .build();
+ final String str = layout.toSerializable(event);
+ assertThat(str).contains(validXmlChars).doesNotContain("\uFFFD");
+ }
+}
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/ssl/SslConfigurationTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/ssl/SslConfigurationTest.java
index 93d02712d5..b916f4410e 100644
---
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/ssl/SslConfigurationTest.java
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/ssl/SslConfigurationTest.java
@@ -16,6 +16,7 @@
*/
package org.apache.logging.log4j.core.net.ssl;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -25,6 +26,11 @@ import java.io.OutputStream;
import java.net.UnknownHostException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
+import org.apache.logging.log4j.core.config.Node;
+import org.apache.logging.log4j.core.config.plugins.util.PluginBuilder;
+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.test.junit.UsingStatusListener;
import org.junit.jupiter.api.Test;
@@ -138,4 +144,19 @@ class SslConfigurationTest {
final SSLSocketFactory factory =
sslConf.getSslContext().getSocketFactory();
assertNotNull(factory);
}
+
+ @Test
+ void verifyHostNameFromXml() {
+ PluginManager pluginManager = new PluginManager(Node.CATEGORY);
+ pluginManager.collectPlugins();
+ PluginType<?> pluginType = pluginManager.getPluginType("Ssl");
+ assertThat(pluginType).isNotNull();
+ Node ssl = new Node(null, pluginType.getElementName(), pluginType);
+ ssl.getAttributes().put("verifyHostName", "true");
+ PluginBuilder builder = new PluginBuilder(pluginType);
+ SslConfiguration sslConfiguration = (SslConfiguration)
builder.withConfigurationNode(ssl)
+ .withConfiguration(new DefaultConfiguration())
+ .build();
+ assertThat(sslConfiguration.isVerifyHostName()).isTrue();
+ }
}
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/TransformTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/TransformTest.java
new file mode 100644
index 0000000000..a9a79e2afb
--- /dev/null
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/TransformTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.util;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.stream.Stream;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class TransformTest {
+
+ static Stream<Arguments> testEscapeHtmlTags() {
+ final char replacement = '\uFFFD';
+ return Stream.of(
+ // Empty
+ Arguments.of("", ""),
+ // characters that need to be escaped
+ Arguments.of("<\"Salt&Peppa'\">",
"<"Salt&Peppa'">"),
+ // control character replaced with U+FFFD
+ Arguments.of("A" + (char) 0x01 + "B", "A" + replacement + "B"),
+ // standalone low surrogate replaced with U+FFFD
+ Arguments.of("low" + Character.MIN_SURROGATE + "surrogate",
"low" + replacement + "surrogate"),
+ Arguments.of(Character.MIN_SURROGATE + "low", replacement +
"low"),
+ // standalone high surrogate replaced with U+FFFD
+ Arguments.of("high" + Character.MAX_SURROGATE + "surrogate",
"high" + replacement + "surrogate"),
+ Arguments.of(Character.MAX_SURROGATE + "high", replacement +
"high"),
+ // FFFE and FFFF
+ Arguments.of("invalid\uFFFEchars", "invalid" + replacement +
"chars"),
+ Arguments.of("invalid\uFFFFchars", "invalid" + replacement +
"chars"),
+ // whitespace characters are preserved
+ Arguments.of("tab\tnewline\ncr\r", "tab\tnewline\ncr\r"),
+ // character beyond BMP (emoji) preserved as surrogate pair
+ Arguments.of("emoji " + "\uD83D\uDE00" + " end", "emoji " +
"\uD83D\uDE00" + " end"));
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void testEscapeHtmlTags(final String input, final String expected) {
+ String actual = Transform.escapeHtmlTags(input);
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ static Stream<Arguments> testAppendEscapingCData() {
+ final char replacement = '\uFFFD';
+ return Stream.of(
+ // Empty
+ Arguments.of("", ""),
+ // characters that need to be escaped
+ Arguments.of("<\"Salt&Peppa'\">", "<\"Salt&Peppa'\">"),
+ // control character replaced with U+FFFD
+ Arguments.of("A" + (char) 0x01 + "B", "A" + replacement + "B"),
+ // standalone low surrogate replaced with U+FFFD
+ Arguments.of("low" + Character.MIN_SURROGATE + "surrogate",
"low" + replacement + "surrogate"),
+ Arguments.of(Character.MIN_SURROGATE + "low", replacement +
"low"),
+ // standalone high surrogate replaced with U+FFFD
+ Arguments.of("high" + Character.MAX_SURROGATE + "surrogate",
"high" + replacement + "surrogate"),
+ Arguments.of(Character.MAX_SURROGATE + "high", replacement +
"high"),
+ // FFFE and FFFF
+ Arguments.of("invalid\uFFFEchars", "invalid" + replacement +
"chars"),
+ Arguments.of("invalid\uFFFFchars", "invalid" + replacement +
"chars"),
+ // whitespace characters are preserved
+ Arguments.of("tab\tnewline\ncr\r", "tab\tnewline\ncr\r"),
+ // character beyond BMP (emoji) preserved as surrogate pair
+ Arguments.of("emoji " + "\uD83D\uDE00" + " end", "emoji " +
"\uD83D\uDE00" + " end"));
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void testAppendEscapingCData(final String input, final String expected) {
+ StringBuilder cdata = new StringBuilder();
+ Transform.appendEscapingCData(cdata, input);
+ assertThat(cdata.toString()).isEqualTo(expected);
+ }
+}
diff --git
a/log4j-core-test/src/test/resources/TlsSocketAppenderTest/log4j2.xml
b/log4j-core-test/src/test/resources/TlsSocketAppenderTest/log4j2.xml
new file mode 100644
index 0000000000..b5f8e25a74
--- /dev/null
+++ b/log4j-core-test/src/test/resources/TlsSocketAppenderTest/log4j2.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+<Configuration xmlns="https://logging.apache.org/xml/ns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="https://logging.apache.org/xml/ns
https://logging.apache.org/xml/ns/log4j-config-2.xsd">
+ <Appenders>
+ <Socket name="SOCKET-${test:server.host}"
+ host="${test:server.host}"
+ port="${test:server.port}"
+ protocol="SSL">
+ <Ssl verifyHostName="${test:ssl.verifyHostname:-true}">
+ <KeyStore location="${test:keystore.location}"
+ password="${test:keystore.password}"
+ type="${test:keystore.type}"/>
+ <TrustStore location="${test:truststore.location}"
+ password="${test:truststore.password}"
+ type="${test:truststore.type}"/>
+ </Ssl>
+ <PatternLayout pattern="%m%n"/>
+ </Socket>
+ </Appenders>
+ <Loggers>
+ <Root level="INFO">
+ <AppenderRef ref="SOCKET-${test:server.host}"/>
+ </Root>
+ </Loggers>
+</Configuration>
diff --git a/log4j-core/pom.xml b/log4j-core/pom.xml
index 49293f47df..b2f34d4ab9 100644
--- a/log4j-core/pom.xml
+++ b/log4j-core/pom.xml
@@ -66,7 +66,7 @@
org.apache.commons.compress.*;resolution:=optional,
org.apache.commons.csv;resolution:=optional,
org.apache.kafka.*;resolution:=optional,
- org.codehaus.stax2;resolution:=optional,
+ org.codehaus.stax2.*;resolution:=optional,
org.jctools.*;resolution:=optional,
org.zeromq;resolution:=optional,
javax.lang.model.*;resolution:=optional,
@@ -217,6 +217,11 @@
<scope>runtime</scope>
<optional>true</optional>
</dependency>
+ <dependency>
+ <groupId>org.codehaus.woodstox</groupId>
+ <artifactId>stax2-api</artifactId>
+ <optional>true</optional>
+ </dependency>
</dependencies>
<build>
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/Log4jXmlObjectMapper.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/Log4jXmlObjectMapper.java
index fa36d1d425..f1b0293a01 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/Log4jXmlObjectMapper.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/Log4jXmlObjectMapper.java
@@ -17,8 +17,22 @@
package org.apache.logging.log4j.core.jackson;
import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.ObjectCodec;
+import com.fasterxml.jackson.core.io.IOContext;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.xml.XmlFactory;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+import com.fasterxml.jackson.dataformat.xml.XmlNameProcessor;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import org.codehaus.stax2.XMLStreamWriter2;
+import org.codehaus.stax2.ri.Stax2WriterAdapter;
+import org.codehaus.stax2.util.StreamWriter2Delegate;
/**
* A Jackson XML {@link ObjectMapper} initialized for Log4j.
@@ -41,7 +55,166 @@ public class Log4jXmlObjectMapper extends XmlMapper {
* Create a new instance using the {@link Log4jXmlModule}.
*/
public Log4jXmlObjectMapper(final boolean includeStacktrace, final boolean
stacktraceAsString) {
- super(new Log4jXmlModule(includeStacktrace, stacktraceAsString));
+ super(new SanitizingXmlFactory(), new
Log4jXmlModule(includeStacktrace, stacktraceAsString));
this.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
}
+
+ /**
+ * Writer that sanitizes text to be valid XML 1.0 by replacing disallowed
code points with the replacement character (U+FFFD).
+ */
+ private static final class SanitizingWriter extends StreamWriter2Delegate {
+
+ private static final char REPLACEMENT_CHAR = '\uFFFD';
+
+ SanitizingWriter(final XMLStreamWriter2 delegate) {
+ super(delegate);
+ setParent(delegate);
+ }
+
+ @Override
+ public void writeAttribute(final String localName, final String value)
throws XMLStreamException {
+ super.writeAttribute(localName, sanitizeXml10(value));
+ }
+
+ @Override
+ public void writeAttribute(final String namespaceURI, final String
localName, final String value)
+ throws XMLStreamException {
+ super.writeAttribute(namespaceURI, localName,
sanitizeXml10(value));
+ }
+
+ @Override
+ public void writeAttribute(
+ final String prefix, final String namespaceURI, final String
localName, final String value)
+ throws XMLStreamException {
+ super.writeAttribute(prefix, namespaceURI, localName,
sanitizeXml10(value));
+ }
+
+ @Override
+ public void writeCData(String text) throws XMLStreamException {
+ super.writeCData(sanitizeXml10(text));
+ }
+
+ @Override
+ public void writeCData(char[] text, int start, int len) throws
XMLStreamException {
+ super.writeCData(sanitizeXml10(text, start, len));
+ }
+
+ @Override
+ public void writeCharacters(final String text) throws
XMLStreamException {
+ super.writeCharacters(sanitizeXml10(text));
+ }
+
+ @Override
+ public void writeCharacters(final char[] text, final int start, final
int len) throws XMLStreamException {
+ super.writeCharacters(sanitizeXml10(text, start, len));
+ }
+
+ @Override
+ public void writeComment(String text) throws XMLStreamException {
+ super.writeComment(sanitizeXml10(text));
+ }
+
+ private static String sanitizeXml10(final String input) {
+ if (input == null) {
+ return null;
+ }
+ final int length = input.length();
+ // Only create a new string if we find an invalid code point.
+ // In the common case, this should avoid unnecessary allocations.
+ for (int i = 0; i < length; ) {
+ final int cp = input.codePointAt(i);
+ if (!isValidXml10(cp)) {
+ final StringBuilder out = new StringBuilder(length);
+ out.append(input, 0, i);
+ appendSanitized(input, i, length, out);
+ return out.toString();
+ }
+ i += Character.charCount(cp);
+ }
+ return input;
+ }
+
+ private static String sanitizeXml10(final char[] input, final int
start, final int len) {
+ return sanitizeXml10(new String(input, start, len));
+ }
+
+ private static void appendSanitized(final String input, int i, final
int length, final StringBuilder out) {
+ while (i < length) {
+ final int cp = input.codePointAt(i);
+ out.appendCodePoint(isValidXml10(cp) ? cp : REPLACEMENT_CHAR);
+ i += Character.charCount(cp);
+ }
+ }
+
+ /**
+ * Checks if a code point is valid
+ *
+ * @param codePoint a code point between {@code 0} and {@link
Character#MAX_CODE_POINT}
+ * @return {@code true} if it is a valid XML 1.0 code point
+ */
+ private static boolean isValidXml10(final int codePoint) {
+ assert codePoint >= 0 && codePoint <= Character.MAX_CODE_POINT;
+ // XML 1.0 valid characters (Fifth Edition):
+ // #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] |
[#x10000-#x10FFFF]
+
+ // [#x20–#xD7FF] (placed early as a fast path for the most common
case)
+ return (codePoint >= ' ' && codePoint < Character.MIN_SURROGATE)
+ // #x9
+ || codePoint == '\t'
+ // #xA
+ || codePoint == '\n'
+ // #xD
+ || codePoint == '\r'
+ // [#xE000-#xFFFD]
+ || (codePoint > Character.MAX_SURROGATE && codePoint <=
0xFFFD)
+ // [#x10000-#x10FFFF]
+ || codePoint >= Character.MIN_SUPPLEMENTARY_CODE_POINT;
+ }
+ }
+
+ /**
+ * Factory that creates {@link SanitizingWriter} instances to ensure that
all text written to the XML output is valid XML 1.0.
+ */
+ private static final class SanitizingXmlFactory extends XmlFactory {
+
+ private static final long serialVersionUID = 1L;
+
+ public SanitizingXmlFactory() {
+ super();
+ }
+
+ private SanitizingXmlFactory(
+ ObjectCodec oc,
+ int xpFeatures,
+ int xgFeatures,
+ XMLInputFactory xmlIn,
+ XMLOutputFactory xmlOut,
+ String nameForTextElem,
+ XmlNameProcessor nameProcessor) {
+ super(oc, xpFeatures, xgFeatures, xmlIn, xmlOut, nameForTextElem,
nameProcessor);
+ }
+
+ @Override
+ protected XMLStreamWriter _createXmlWriter(final IOContext ctxt, final
Writer w) throws IOException {
+ return new
SanitizingWriter(Stax2WriterAdapter.wrapIfNecessary(super._createXmlWriter(ctxt,
w)));
+ }
+
+ @Override
+ protected XMLStreamWriter _createXmlWriter(final IOContext ctxt, final
OutputStream out) throws IOException {
+ return new
SanitizingWriter(Stax2WriterAdapter.wrapIfNecessary(super._createXmlWriter(ctxt,
out)));
+ }
+
+ @Override
+ public XmlFactory copy() {
+ _checkInvalidCopy(SanitizingXmlFactory.class);
+ return new SanitizingXmlFactory(
+ _objectCodec,
+ _xmlParserFeatures,
+ _xmlGeneratorFeatures,
+ _xmlInputFactory,
+ _xmlOutputFactory,
+ _cfgNameForTextElement,
+ _nameProcessor);
+ }
+ }
}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/Rfc5424Layout.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/Rfc5424Layout.java
index 256ce20f28..6a1ba5da0c 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/Rfc5424Layout.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/Rfc5424Layout.java
@@ -24,6 +24,7 @@ import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Matcher;
@@ -96,6 +97,8 @@ public final class Rfc5424Layout extends AbstractStringLayout
{
*/
public static final String DEFAULT_MDCID = "mdc";
+ private static final int SD_PARAM_NAME_MAX_LENGTH = 32;
+
private static final String LF = "\n";
private static final int TWO_DIGITS = 10;
private static final int THREE_DIGITS = 100;
@@ -467,6 +470,26 @@ public final class Rfc5424Layout extends
AbstractStringLayout {
return mdcIncludes;
}
+ // Test-only
+ List<String> getMdcRequired() {
+ return mdcRequired;
+ }
+
+ // Test-only
+ boolean isIncludeNewLine() {
+ return includeNewLine;
+ }
+
+ // Test-only
+ String getEscapeNewLine() {
+ return escapeNewLine;
+ }
+
+ // Test-only
+ boolean isUseTlsMessageFormat() {
+ return useTlsMessageFormat;
+ }
+
private String computeTimeStampString(final long now) {
long last;
synchronized (this) {
@@ -579,14 +602,70 @@ public final class Rfc5424Layout extends
AbstractStringLayout {
if (prefix != null) {
sb.append(prefix);
}
- final String safeKey =
escapeNewlines(escapeSDParams(entry.getKey()), escapeNewLine);
- final String safeValue =
escapeNewlines(escapeSDParams(entry.getValue()), escapeNewLine);
+ // No need to escape new lines, since parameter names cannot
contain them.
+ final String safeKey = sanitizeParamName(entry.getKey());
+ final String safeValue =
escapeNewlines(escapeParamValue(entry.getValue()), escapeNewLine);
StringBuilders.appendKeyDqValue(sb, safeKey, safeValue);
}
}
}
- private String escapeSDParams(final String value) {
+ /**
+ * Sanitizes an RFC 5424 {@code PARAM-NAME}
+ *
+ * <p>Invalid characters are replaced with {@code '?'} and the result is
truncated to
+ * {@value #SD_PARAM_NAME_MAX_LENGTH}.</p>
+ *
+ * @param key the original parameter name
+ * @return a sanitized parameter name compliant with RFC 5424
+ */
+ private String sanitizeParamName(final String key) {
+ final int length = key.length();
+ if (length == 0) {
+ return "?";
+ }
+ if (length > SD_PARAM_NAME_MAX_LENGTH) {
+ return sanitizeParamNameSlowPath(key);
+ }
+ for (int i = 0; i < length; i++) {
+ if (!isParamNameCharacterValid(key.charAt(i))) {
+ return sanitizeParamNameSlowPath(key);
+ }
+ }
+ return key;
+ }
+
+ private String sanitizeParamNameSlowPath(final String key) {
+ final StringBuilder sb = new StringBuilder(SD_PARAM_NAME_MAX_LENGTH);
+ final int maxLength = Math.min(key.length(), SD_PARAM_NAME_MAX_LENGTH);
+ for (int i = 0; i < maxLength; i++) {
+ final char c = key.charAt(i);
+ sb.append(isParamNameCharacterValid(c) ? c : '?');
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Checks whether a character is allowed in an RFC 5424 {@code PARAM-NAME}.
+ *
+ * <p>Valid characters are printable US-ASCII characters
+ * ({@code 0x20-0x7E}) excluding:
+ *
+ * <ul>
+ * <li>{@code '='} – parameter delimiter</li>
+ * <li>{@code ' '} – not permitted in SD-NAME</li>
+ * <li>{@code ']'} – structured data terminator</li>
+ * <li>{@code '"'} – quoting delimiter</li>
+ * </ul>
+ *
+ * @param c the character to test
+ * @return {@code true} if the character is allowed in an {@code SD-NAME}
+ */
+ private static boolean isParamNameCharacterValid(final char c) {
+ return c > 32 && c <= 126 && c != '=' && c != ']' && c != '"';
+ }
+
+ private String escapeParamValue(final String value) {
StringBuilder output = null;
for (int i = 0; i < value.length(); i++) {
final char cur = value.charAt(i);
@@ -639,7 +718,7 @@ public final class Rfc5424Layout extends
AbstractStringLayout {
* @param loggerFields Container for the KeyValuePairs containing the
patterns
* @param config The Configuration. Some Converters require access to the
Interpolator.
* @return An Rfc5424Layout.
- * @deprecated Use {@link Rfc5424LayoutBuilder instead}
+ * @deprecated Since 2.21.0 use {@link Rfc5424LayoutBuilder instead}
*/
@Deprecated
public static Rfc5424Layout createLayout(
@@ -689,65 +768,208 @@ public final class Rfc5424Layout extends
AbstractStringLayout {
.build();
}
+ /**
+ * @since 2.21.0
+ */
@PluginBuilderFactory
public static Rfc5424LayoutBuilder newBuilder() {
return new Rfc5424LayoutBuilder();
}
+ /**
+ * @since 2.21.0
+ */
public static class Rfc5424LayoutBuilder extends
AbstractStringLayout.Builder<Rfc5424LayoutBuilder>
implements
org.apache.logging.log4j.core.util.Builder<Rfc5424Layout> {
+ /**
+ * The name of the {@link Facility} as described in RFC 5424
+ *
+ * <p>The matching is case-insensitive. Defaults to {@code LOCAL0}.</p>
+ */
@PluginBuilderAttribute
private Facility facility = Facility.LOCAL0;
+ /**
+ * The default {@code SD-ID} as described in RFC 5424.
+ */
@PluginBuilderAttribute
private String id;
+ /**
+ * The enterprise number to include in {@code SD-ID} identifiers.
+ *
+ * <p>Can contain multiple integers separated by a dot, for example
{@code 32473.1}</p>
+ *
+ * <p>Defaults to {@value #DEFAULT_ENTERPRISE_NUMBER}.</p>
+ */
@PluginBuilderAttribute
private String ein = String.valueOf(DEFAULT_ENTERPRISE_NUMBER);
+ /**
+ * The enterprise number to include in {@code SD-ID} identifiers.
+ *
+ * <p>Limited to a single integer.</p>
+ *
+ * <p>Defaults to {@value #DEFAULT_ENTERPRISE_NUMBER}.</p>
+ */
@PluginBuilderAttribute
private Integer enterpriseNumber;
+ /**
+ * Indicates whether data from the context map will be included as RFC
5424 {@code SD-ELEMENT}.
+ *
+ * <p>Defaults to {@code true}.</p>
+ */
@PluginBuilderAttribute
private boolean includeMDC = true;
+ /**
+ * If {@code true}, a newline will be appended to the end of the
syslog record.
+ *
+ * <p>Default is {@code false}.</p>
+ */
+ @SuppressWarnings("log4j.public.setter")
+ @PluginBuilderAttribute
+ private boolean newLine;
+
+ /**
+ * Same as {@code newLine}.
+ *
+ * <p>Erroneously introduced in version 2.21.0, but kept for
compatibility.</p>
+ */
@PluginBuilderAttribute
private boolean includeNL;
+ /**
+ * If set, this string will be used to replace new lines within the
message text.
+ *
+ * <p>By default, new lines are not escaped.</p>
+ */
+ @SuppressWarnings("log4j.public.setter")
+ @PluginBuilderAttribute
+ private String newLineEscape;
+
+ /**
+ * Same as {@code newLineEscape}.
+ *
+ * <p>Erroneously introduced in version 2.21.0, but kept for
compatibility.</p>
+ */
@PluginBuilderAttribute
private String escapeNL;
+ /**
+ * The id to use for the MDC Structured Data Element.
+ *
+ * <p>Defaults to {@value #DEFAULT_MDCID}.</p>
+ */
@PluginBuilderAttribute
private String mdcId = DEFAULT_MDCID;
+ /**
+ * A prefix to add to MDC key names when formatting them as structured
data parameters.
+ */
@PluginBuilderAttribute
private String mdcPrefix;
+ /**
+ * A prefix to add to event key names when formatting {@link
StructuredDataMessage} fields.
+ */
@PluginBuilderAttribute
private String eventPrefix;
+ /**
+ * The value to use as the {@code APP-NAME} in the RFC 5424 syslog
record.
+ */
@PluginBuilderAttribute
private String appName;
+ /**
+ * The default value to be used in the {@code MSGID} field of RFC 5424
syslog records.
+ *
+ * <p>If the log event contains a {@link StructuredDataMessage}, the
id from that message will be used
+ * instead.</p>
+ */
@PluginBuilderAttribute
private String messageId;
+ /**
+ * A comma separated list of MDC keys that should be excluded from the
LogEvent.
+ *
+ * <p>Mutually exclusive with {@link #mdcIncludes}.</p>
+ */
+ @SuppressWarnings("log4j.public.setter")
+ @PluginBuilderAttribute
+ private String mdcExcludes;
+
+ /**
+ * Same as {@code mdcExcludes}.
+ *
+ * <p>Erroneously introduced in version 2.21.0, but kept for
compatibility.</p>
+ */
@PluginBuilderAttribute
private String excludes;
+ /**
+ * A comma separated list of MDC keys that should be included in the
LogEvent.
+ *
+ * <p>Mutually exclusive with {@link #mdcExcludes}.</p>
+ */
+ @SuppressWarnings("log4j.public.setter")
+ @PluginBuilderAttribute
+ private String mdcIncludes;
+
+ /**
+ * Same as {@code mdcIncludes}.
+ *
+ * <p>Erroneously introduced in version 2.21.0, but kept for
compatibility.</p>
+ */
@PluginBuilderAttribute
private String includes;
+ /**
+ * A comma separated list of MDC keys that must be present in the MDC.
+ */
+ @SuppressWarnings("log4j.public.setter")
+ @PluginBuilderAttribute
+ private String mdcRequired;
+
+ /**
+ * Same as {@code mdcRequired}.
+ *
+ * <p>Erroneously introduced in version 2.21.0, but kept for
compatibility.</p>
+ */
@PluginBuilderAttribute
private String required;
+ /**
+ * The pattern used to format exceptions appended to the syslog
message.
+ */
@PluginBuilderAttribute
private String exceptionPattern;
+ /**
+ * If true the message will be formatted according to RFC 5425.
+ *
+ * <p>Default is {@code false}.</p>
+ */
+ @SuppressWarnings("log4j.public.setter")
+ @PluginBuilderAttribute
+ private boolean useTlsMessageFormat;
+
+ /**
+ * Same as {@code useTlsMessageFormat}.
+ *
+ * <p>Erroneously introduced in version 2.21.0, but kept for
compatibility.</p>
+ */
@PluginBuilderAttribute
private boolean useTLSMessageFormat;
+ /**
+ * Optional additional {@code SD-ELEMENT}s.
+ *
+ * <p>Each {@link LoggerFields} entry contains a set of key/value
patterns to produce structured data parameters.</p>
+ */
@PluginElement(value = "loggerFields")
private LoggerFields[] loggerFields;
@@ -860,9 +1082,12 @@ public final class Rfc5424Layout extends
AbstractStringLayout {
@Override
public Rfc5424Layout build() {
- if (includes != null && excludes != null) {
- LOGGER.error("mdcIncludes and mdcExcludes are mutually
exclusive. Includes wil be ignored");
- includes = null;
+ String effectiveIncludes = Objects.toString(mdcIncludes, includes);
+ String effectiveExcludes = Objects.toString(mdcExcludes, excludes);
+
+ if (effectiveIncludes != null && effectiveExcludes != null) {
+ LOGGER.error("mdcIncludes and mdcExcludes are mutually
exclusive. Includes will be ignored");
+ effectiveIncludes = null;
}
if (enterpriseNumber != null) {
@@ -880,19 +1105,19 @@ public final class Rfc5424Layout extends
AbstractStringLayout {
id,
ein,
includeMDC,
- includeNL,
- escapeNL,
+ newLine || includeNL,
+ Objects.toString(newLineEscape, escapeNL),
mdcId,
mdcPrefix,
eventPrefix,
appName,
messageId,
- excludes,
- includes,
- required,
+ effectiveExcludes,
+ effectiveIncludes,
+ Objects.toString(mdcRequired, required),
charset != null ? charset : StandardCharsets.UTF_8,
exceptionPattern,
- useTLSMessageFormat,
+ useTlsMessageFormat || useTLSMessageFormat,
loggerFields);
}
}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/net/ssl/SslConfiguration.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/net/ssl/SslConfiguration.java
index a27febcabb..1d4065f9af 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/net/ssl/SslConfiguration.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/net/ssl/SslConfiguration.java
@@ -180,13 +180,10 @@ public class SslConfiguration {
* @return a new SslConfiguration
*/
@NullUnmarked
- @PluginFactory
public static SslConfiguration createSSLConfiguration(
- // @formatter:off
- @PluginAttribute("protocol") final String protocol,
- @PluginElement("KeyStore") final KeyStoreConfiguration
keyStoreConfig,
- @PluginElement("TrustStore") final TrustStoreConfiguration
trustStoreConfig) {
- // @formatter:on
+ final String protocol,
+ final KeyStoreConfiguration keyStoreConfig,
+ final TrustStoreConfiguration trustStoreConfig) {
return new SslConfiguration(protocol, false, keyStoreConfig,
trustStoreConfig);
}
@@ -201,6 +198,7 @@ public class SslConfiguration {
* @since 2.12
*/
@NullUnmarked
+ @PluginFactory
public static SslConfiguration createSSLConfiguration(
// @formatter:off
@PluginAttribute("protocol") final String protocol,
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Transform.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Transform.java
index e9eb2a5f39..136955448d 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Transform.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Transform.java
@@ -29,59 +29,41 @@ public final class Transform {
private static final String CDATA_EMBEDED_END = CDATA_END +
CDATA_PSEUDO_END + CDATA_START;
private static final int CDATA_END_LEN = CDATA_END.length();
+ private static final char REPLACEMENT_CHAR = '\uFFFD';
+
private Transform() {}
/**
- * This method takes a string which may contain HTML tags (ie,
- * <b>, <table>, etc) and replaces any
- * '<', '>' , '&' or '"'
- * characters with respective predefined entity references.
+ * Escapes characters in a string for safe inclusion in HTML or XML text.
*
- * @param input The text to be converted.
- * @return The input string with the special characters replaced.
+ * <p>Replaces the characters {@code <}, {@code >}, {@code &}, {@code "}
and {@code '} with their corresponding
+ * entity references ({@code <}, {@code >}, {@code &}, {@code
"}, and {@code '}). Any code point
+ * that is invalid in XML 1.0 is replaced with the Unicode replacement
character U+FFFD.</p>
+ *
+ * @param input The text to be escaped; may be {@code null} or empty.
+ * @return The escaped string, or the original {@code input} if no changes
were required.
*/
public static String escapeHtmlTags(final String input) {
- // Check if the string is null, zero length or devoid of special
characters
+ // Check if the string is null or zero length
// if so, return what was sent in.
-
- if (Strings.isEmpty(input)
- || (input.indexOf('"') == -1
- && input.indexOf('&') == -1
- && input.indexOf('<') == -1
- && input.indexOf('>') == -1)) {
+ if (Strings.isEmpty(input)) {
return input;
}
- // Use a StringBuilder in lieu of String concatenation -- it is
- // much more efficient this way.
-
- final StringBuilder buf = new StringBuilder(input.length() + 6);
-
- final int len = input.length();
- for (int i = 0; i < len; i++) {
- final char ch = input.charAt(i);
- if (ch > '>') {
- buf.append(ch);
- } else
- switch (ch) {
- case '<':
- buf.append("<");
- break;
- case '>':
- buf.append(">");
- break;
- case '&':
- buf.append("&");
- break;
- case '"':
- buf.append(""");
- break;
- default:
- buf.append(ch);
- break;
- }
+ // Only create a new string if we find a special character or invalid
code point.
+ // In the common case, this should avoid unnecessary allocations.
+ final int length = input.length();
+ for (int i = 0; i < length; ) {
+ final int cp = input.codePointAt(i);
+ if (!isValidXml10(cp) || isHtmlTagCharacter(cp)) {
+ final StringBuilder out = new StringBuilder(length);
+ out.append(input, 0, i);
+ appendEscapingHtmlTags(input, i, length, out);
+ return out.toString();
+ }
+ i += Character.charCount(cp);
}
- return buf.toString();
+ return input;
}
/**
@@ -97,11 +79,11 @@ public final class Transform {
if (str != null) {
int end = str.indexOf(CDATA_END);
if (end < 0) {
- buf.append(str);
+ appendSanitizedXml10(str, 0, str.length(), buf);
} else {
int start = 0;
while (end > -1) {
- buf.append(str.substring(start, end));
+ appendSanitizedXml10(str, start, end, buf);
buf.append(CDATA_EMBEDED_END);
start = end + CDATA_END_LEN;
if (start < str.length()) {
@@ -110,7 +92,7 @@ public final class Transform {
return;
}
}
- buf.append(str.substring(start));
+ appendSanitizedXml10(str, start, str.length(), buf);
}
}
}
@@ -185,4 +167,69 @@ public final class Transform {
}
return buf.toString();
}
+
+ private static void appendEscapingHtmlTags(final String input, int i,
final int length, final StringBuilder buf) {
+ while (i < length) {
+ final int ch = input.codePointAt(i);
+ switch (ch) {
+ case '<':
+ buf.append("<");
+ break;
+ case '>':
+ buf.append(">");
+ break;
+ case '&':
+ buf.append("&");
+ break;
+ case '"':
+ buf.append(""");
+ break;
+ case '\'':
+ buf.append("'");
+ break;
+ default:
+ buf.appendCodePoint(isValidXml10(ch) ? ch :
REPLACEMENT_CHAR);
+ break;
+ }
+ i += Character.charCount(ch);
+ }
+ }
+
+ private static boolean isHtmlTagCharacter(final int cp) {
+ return cp == '<' || cp == '>' || cp == '&' || cp == '"' || cp == '\'';
+ }
+
+ private static void appendSanitizedXml10(
+ final String input, final int start, final int end, final
StringBuilder out) {
+ for (int i = start; i < end; ) {
+ final int cp = input.codePointAt(i);
+ out.appendCodePoint(isValidXml10(cp) ? cp : REPLACEMENT_CHAR);
+ i += Character.charCount(cp);
+ }
+ }
+
+ /**
+ * Checks if a code point is valid in XML 1.0.
+ *
+ * @param codePoint a code point between {@code 0} and {@link
Character#MAX_CODE_POINT}
+ * @return {@code true} if it is a valid XML 1.0 code point
+ */
+ private static boolean isValidXml10(final int codePoint) {
+ assert codePoint >= 0 && codePoint <= Character.MAX_CODE_POINT;
+ // XML 1.0 valid characters (Fifth Edition):
+ // #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] |
[#x10000-#x10FFFF]
+
+ // [#x20–#xD7FF] (placed early as a fast path for the most common case)
+ return (codePoint >= ' ' && codePoint < Character.MIN_SURROGATE)
+ // #x9
+ || codePoint == '\t'
+ // #xA
+ || codePoint == '\n'
+ // #xD
+ || codePoint == '\r'
+ // [#xE000-#xFFFD]
+ || (codePoint > Character.MAX_SURROGATE && codePoint <= 0xFFFD)
+ // [#x10000-#x10FFFF]
+ || codePoint >= Character.MIN_SUPPLEMENTARY_CODE_POINT;
+ }
}
diff --git
a/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/util/JsonWriterTest.java
b/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/util/JsonWriterTest.java
index 11607acb2d..b390449714 100644
---
a/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/util/JsonWriterTest.java
+++
b/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/util/JsonWriterTest.java
@@ -40,6 +40,8 @@ import org.apache.logging.log4j.util.Strings;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
@SuppressWarnings({"DoubleBraceInitialization", "UnnecessaryStringBuilder"})
class JsonWriterTest {
@@ -606,6 +608,14 @@ class JsonWriterTest {
}
}
+ @ParameterizedTest
+ @ValueSource(floats = {Float.NEGATIVE_INFINITY, Float.NaN,
Float.POSITIVE_INFINITY})
+ void test_writeNumber_float_non_finite(final float number) {
+ final String expectedJson = "\"" + number + "\"";
+ final String actualJson = withLockedWriterReturning(writer ->
writer.use(() -> writer.writeNumber(number)));
+ Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+ }
+
@Test
void test_writeNumber_float() {
for (final float number : new float[] {Float.MIN_VALUE, -1.0F, 0F,
1.0F, Float.MAX_VALUE}) {
@@ -615,6 +625,14 @@ class JsonWriterTest {
}
}
+ @ParameterizedTest
+ @ValueSource(doubles = {Double.NEGATIVE_INFINITY, Double.NaN,
Double.POSITIVE_INFINITY})
+ void test_writeNumber_double_non_finite(final double number) {
+ final String expectedJson = "\"" + number + "\"";
+ final String actualJson = withLockedWriterReturning(writer ->
writer.use(() -> writer.writeNumber(number)));
+ Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+ }
+
@Test
void test_writeNumber_double() {
for (final double number : new double[] {Double.MIN_VALUE, -1.0D, 0D,
1.0D, Double.MAX_VALUE}) {
diff --git
a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/JsonWriter.java
b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/JsonWriter.java
index 749eb1519e..5fbf72c6b0 100644
---
a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/JsonWriter.java
+++
b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/JsonWriter.java
@@ -722,11 +722,21 @@ public final class JsonWriter implements AutoCloseable,
Cloneable {
}
public void writeNumber(final float number) {
- stringBuilder.append(number);
+ // Follows the same logic as Jackson's
JsonWriteFeature#WRITE_NAN_AS_STRINGS feature.
+ if (!Float.isFinite(number)) {
+ writeString(Float.toString(number));
+ } else {
+ stringBuilder.append(number);
+ }
}
public void writeNumber(final double number) {
- stringBuilder.append(number);
+ // Follows the same logic as Jackson's
JsonWriteFeature#WRITE_NAN_AS_STRINGS feature.
+ if (!Double.isFinite(number)) {
+ writeString(Double.toString(number));
+ } else {
+ stringBuilder.append(number);
+ }
}
public void writeNumber(final short number) {
diff --git a/log4j-parent/pom.xml b/log4j-parent/pom.xml
index dcb10255a4..9a7b5aa2c8 100644
--- a/log4j-parent/pom.xml
+++ b/log4j-parent/pom.xml
@@ -134,6 +134,7 @@
<plexus-utils.version>3.6.0</plexus-utils.version>
<spring-boot.version>2.7.18</spring-boot.version>
<spring-framework.version>5.3.39</spring-framework.version>
+ <stax2-api.version>4.2.2</stax2-api.version>
<system-stubs.version>2.0.3</system-stubs.version>
<velocity.version>1.7</velocity.version>
<wiremock.version>2.35.2</wiremock.version>
@@ -790,6 +791,12 @@
</exclusions>
</dependency>
+ <dependency>
+ <groupId>org.codehaus.woodstox</groupId>
+ <artifactId>stax2-api</artifactId>
+ <version>${stax2-api.version}</version>
+ </dependency>
+
<dependency>
<groupId>uk.org.webcompere</groupId>
<artifactId>system-stubs-core</artifactId>
diff --git a/pom.xml b/pom.xml
index c4d4af8b9a..1b4e5c92d3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -279,7 +279,7 @@
<scm child.scm.connection.inherit.append.path="false"
child.scm.developerConnection.inherit.append.path="false"
child.scm.url.inherit.append.path="false">
<connection>scm:git:https://github.com/apache/logging-log4j2.git</connection>
<developerConnection>scm:git:https://github.com/apache/logging-log4j2.git</developerConnection>
- <tag>2.x</tag>
+ <tag>rel/2.25.3</tag>
<url>https://github.com/apache/logging-log4j2</url>
</scm>
@@ -309,9 +309,9 @@
<!-- project version -->
<revision>2.26.0-SNAPSHOT</revision>
<!-- Versions used on the site: no snapshots! -->
- <site-log4j-api.version>2.25.2</site-log4j-api.version>
- <site-log4j-core.version>2.25.2</site-log4j-core.version>
-
<site-log4j-layout-template-json.version>2.25.2</site-log4j-layout-template-json.version>
+ <site-log4j-api.version>2.25.4</site-log4j-api.version>
+ <site-log4j-core.version>2.25.4</site-log4j-core.version>
+
<site-log4j-layout-template-json.version>2.25.4</site-log4j-layout-template-json.version>
<!-- =================
Common properties
@@ -325,7 +325,7 @@
2. This value is employed in various places while creating the
distribution
To mitigate these, we define a *dummy* value here and let the CI
replace it during a release.
Hence, *DO NOT MANUALLY EDIT THIS VALUE*! -->
-
<project.build.outputTimestamp>2025-12-15T12:02:19Z</project.build.outputTimestamp>
+
<project.build.outputTimestamp>2026-03-25T07:52:12Z</project.build.outputTimestamp>
<!-- ========================
Site-specific properties
diff --git a/src/changelog/2.25.0/update_com_fasterxml_jackson_jackson_bom.xml
b/src/changelog/2.25.0/update_com_fasterxml_jackson_jackson_bom.xml
index 08757e744a..73d0af758c 100644
--- a/src/changelog/2.25.0/update_com_fasterxml_jackson_jackson_bom.xml
+++ b/src/changelog/2.25.0/update_com_fasterxml_jackson_jackson_bom.xml
@@ -3,6 +3,6 @@
xmlns="https://logging.apache.org/xml/ns"
xsi:schemaLocation="https://logging.apache.org/xml/ns
https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
type="updated">
- <issue id="3847" link="https://github.com/apache/logging-log4j2/pull/3847"/>
- <description format="asciidoc">Update `com.fasterxml.jackson:jackson-bom` to
version ``</description>
+ <issue id="3708" link="https://github.com/apache/logging-log4j2/pull/3708"/>
+ <description format="asciidoc">Update `com.fasterxml.jackson:jackson-bom` to
version `2.19.0`</description>
</entry>
diff --git a/src/changelog/2.25.4/.release-notes.adoc.ftl
b/src/changelog/2.25.4/.release-notes.adoc.ftl
new file mode 100644
index 0000000000..fd06e74e15
--- /dev/null
+++ b/src/changelog/2.25.4/.release-notes.adoc.ftl
@@ -0,0 +1,32 @@
+////
+ 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
+
+ https://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.
+////
+
+[${'#release-notes-' + release.version?replace("[^a-zA-Z0-9]", "-", "r")}]
+== ${release.version}
+
+<#if release.date?has_content>Release date:: ${release.date}</#if>
+
+This patch release delivers fixes for configuration inconsistencies and
formatting issues across several layouts.
+
+* Restores alignment between documented and actual configuration attributes.
+* Fixes formatting and sanitization issues in XML and RFC5424 layouts.
+* Improves handling of invalid characters and non-standard values.
+
+The authoritative list of recognized configuration attributes is available in
the
+{logging-services-url}/log4j/2.x/plugin-reference.html[Plugin Reference].
+
+<#include "../.changelog.adoc.ftl">
diff --git a/src/changelog/2.25.4/.release.xml
b/src/changelog/2.25.4/.release.xml
new file mode 100644
index 0000000000..e007748cb3
--- /dev/null
+++ b/src/changelog/2.25.4/.release.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+<release xmlns="https://logging.apache.org/xml/ns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="https://logging.apache.org/xml/ns
https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+ date="2026-03-28" version="2.25.4"/>
diff --git
a/src/changelog/2.25.4/3975_prevent_warning_for_last_null_argument.xml
b/src/changelog/2.25.4/3975_prevent_warning_for_last_null_argument.xml
new file mode 100644
index 0000000000..01a937e319
--- /dev/null
+++ b/src/changelog/2.25.4/3975_prevent_warning_for_last_null_argument.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ https://logging.apache.org/xml/ns
+ https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+ type="fixed">
+ <issue id="3975"
link="https://github.com/apache/logging-log4j2/issues/3975"/>
+ <issue id="4014"
link="https://github.com/apache/logging-log4j2/pull/4014"/>
+ <description format="asciidoc">
+ Don't issue warnings if extra argument in parameterized logging is
`null`.
+ </description>
+</entry>
diff --git a/src/changelog/2.25.4/4022_rfc5424-param-names.xml
b/src/changelog/2.25.4/4022_rfc5424-param-names.xml
new file mode 100644
index 0000000000..3949415b25
--- /dev/null
+++ b/src/changelog/2.25.4/4022_rfc5424-param-names.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ https://logging.apache.org/xml/ns
+ https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+ type="fixed">
+ <issue id="4022"
link="https://github.com/apache/logging-log4j2/issues/4022"/>
+ <issue id="4074"
link="https://github.com/apache/logging-log4j2/pull/4074"/>
+ <description format="asciidoc">
+ Restore support for documented `Rfc5424Layout` parameter names.
+ </description>
+</entry>
diff --git a/src/changelog/2.25.4/4033_fix_custom_throwable_to_sting.xml
b/src/changelog/2.25.4/4033_fix_custom_throwable_to_sting.xml
new file mode 100644
index 0000000000..8948d8316e
--- /dev/null
+++ b/src/changelog/2.25.4/4033_fix_custom_throwable_to_sting.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ https://logging.apache.org/xml/ns
+ https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+ type="fixed">
+ <issue id="3623"
link="https://github.com/apache/logging-log4j2/issues/3623"/>
+ <issue id="4033"
link="https://github.com/apache/logging-log4j2/pull/4033"/>
+ <description format="asciidoc">
+ Take `Throwable#toString()` into account while rendering stack traces
in Pattern Layout.
+ </description>
+</entry>
diff --git a/src/changelog/2.25.4/4060_resource-loading.xml
b/src/changelog/2.25.4/4060_resource-loading.xml
new file mode 100644
index 0000000000..b858937041
--- /dev/null
+++ b/src/changelog/2.25.4/4060_resource-loading.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ https://logging.apache.org/xml/ns
+ https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+ type="fixed">
+ <issue id="4058"
link="https://github.com/apache/logging-log4j2/issues/4058"/>
+ <issue id="4060"
link="https://github.com/apache/logging-log4j2/pull/4060"/>
+ <description format="asciidoc">
+ Added debug level logs for successful resource loading in `Loader`
class.
+ </description>
+</entry>
diff --git a/src/changelog/2.25.4/4061_ssl-connection.xml
b/src/changelog/2.25.4/4061_ssl-connection.xml
new file mode 100644
index 0000000000..25cea2ddd7
--- /dev/null
+++ b/src/changelog/2.25.4/4061_ssl-connection.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ https://logging.apache.org/xml/ns
+ https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+ type="fixed">
+ <issue id="4061"
link="https://github.com/apache/logging-log4j2/issues/4061"/>
+ <issue id="4075"
link="https://github.com/apache/logging-log4j2/pull/4075"/>
+ <description format="asciidoc">
+ Align `SslConfiguration` factory method usage with Log4j 2.12+ API.
+ The `verifyHostname` attribute is now correctly recognized.
+ </description>
+</entry>
diff --git a/src/changelog/2.25.4/4073_rfc5424-sd-param.xml
b/src/changelog/2.25.4/4073_rfc5424-sd-param.xml
new file mode 100644
index 0000000000..43428736ac
--- /dev/null
+++ b/src/changelog/2.25.4/4073_rfc5424-sd-param.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ https://logging.apache.org/xml/ns
+ https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+ type="fixed">
+ <issue id="4073"
link="https://github.com/apache/logging-log4j2/pull/4073"/>
+ <description format="asciidoc">
+ Fix sanitization of structured data parameter names in RFC5424 layout.
+ </description>
+</entry>
diff --git a/src/changelog/2.25.4/4077_xml-control-characters.xml
b/src/changelog/2.25.4/4077_xml-control-characters.xml
new file mode 100644
index 0000000000..ddae6cb481
--- /dev/null
+++ b/src/changelog/2.25.4/4077_xml-control-characters.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ https://logging.apache.org/xml/ns
+ https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+ type="fixed">
+ <issue id="4077"
link="https://github.com/apache/logging-log4j2/pull/4077"/>
+ <description format="asciidoc">
+ Replace invalid characters in XmlLayout output with the Unicode
replacement character (U+FFFD).
+ </description>
+</entry>
diff --git a/src/changelog/2.25.4/4078_log4j1-xml-control-characters.xml
b/src/changelog/2.25.4/4078_log4j1-xml-control-characters.xml
new file mode 100644
index 0000000000..b9164561e6
--- /dev/null
+++ b/src/changelog/2.25.4/4078_log4j1-xml-control-characters.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ https://logging.apache.org/xml/ns
+ https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+ type="fixed">
+ <issue id="4078"
link="https://github.com/apache/logging-log4j2/pull/4078"/>
+ <description format="asciidoc">
+ Replace invalid characters in Log4j1XmlLayout output with the Unicode
replacement character (U+FFFD).
+ </description>
+</entry>
diff --git a/src/changelog/2.25.4/4079_map-message-control-characters.xml
b/src/changelog/2.25.4/4079_map-message-control-characters.xml
new file mode 100644
index 0000000000..1365e5f167
--- /dev/null
+++ b/src/changelog/2.25.4/4079_map-message-control-characters.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ https://logging.apache.org/xml/ns
+ https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+ type="fixed">
+ <issue id="4079"
link="https://github.com/apache/logging-log4j2/pull/4079"/>
+ <description format="asciidoc">
+ Replace invalid characters in MapMessage.asXml() output with the
Unicode replacement character (U+FFFD).
+ </description>
+</entry>
diff --git a/src/changelog/2.25.4/4080_jtl-nan.xml
b/src/changelog/2.25.4/4080_jtl-nan.xml
new file mode 100644
index 0000000000..962c3db041
--- /dev/null
+++ b/src/changelog/2.25.4/4080_jtl-nan.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ https://logging.apache.org/xml/ns
+ https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+ type="fixed">
+ <issue id="4080"
link="https://github.com/apache/logging-log4j2/pull/4080"/>
+ <description format="asciidoc">
+ Write non-finite floating-point numbers as strings in `JsonWriter`.
+ </description>
+</entry>