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 =
+                
"&lt;&gt;&#39;&quot;&amp;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 
&lt;foo&gt;</Entry>\n" + "</Map>";
-        assertEquals(expected, result);
+
+        assertThat(result)
+                .isEqualTo(
+                        "<Map>\n" //
+                                + "  <Entry key=\"k&lt;e&amp;y&gt; 
&apos;&quot;\t\r\n"
+                                + notBmp
+                                + expectedInvalid
+                                + "\">v&gt;al&lt;u&amp; &apos;&quot;\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 = 
"&lt;&quot;Salt&amp;Peppa&apos;&quot;&gt;";
+    static Stream<Arguments> escapeXmlCharactersCorrectly() {
+        final char replacement = '\uFFFD';
+        return Stream.of(
+                // Empty
+                Arguments.of("", ""),
+                // characters that need to be escaped
+                Arguments.of("<\"Salt&Peppa'\">", 
"&lt;&quot;Salt&amp;Peppa&apos;&quot;&gt;"),
+                // 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'\">", 
"&lt;&quot;Salt&amp;Peppa&#39;&quot;&gt;"),
+                // 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,
-     * &lt;b&gt;, &lt;table&gt;, etc) and replaces any
-     * '&lt;',  '&gt;' , '&amp;' or '&quot;'
-     * 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 &lt;}, {@code &gt;}, {@code &amp;}, {@code 
&quot;}, and {@code &#39;}). 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("&lt;");
-                        break;
-                    case '>':
-                        buf.append("&gt;");
-                        break;
-                    case '&':
-                        buf.append("&amp;");
-                        break;
-                    case '"':
-                        buf.append("&quot;");
-                        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("&lt;");
+                    break;
+                case '>':
+                    buf.append("&gt;");
+                    break;
+                case '&':
+                    buf.append("&amp;");
+                    break;
+                case '"':
+                    buf.append("&quot;");
+                    break;
+                case '\'':
+                    buf.append("&#39;");
+                    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>

Reply via email to