This is an automated email from the ASF dual-hosted git repository.

pkarwasz pushed a commit to branch fix/2.25.x/rfc5424-sd-param
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit 7c4bf23b4a5882fde0952d860373cedbb1b35949
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Tue Mar 10 23:11:10 2026 +0100

    Rfc5424Layout: Fix sanitization of SD parameter names
    
    This change corrects the sanitization of `PARAM-NAME` in RFC 5424 
structured data produced by `Rfc5424Layout`.
    
    Previously, parameter names were sanitized using the same escaping 
mechanism as parameter values. However, RFC 5424 does not define an escape 
mechanism for `PARAM-NAME`; instead, names must follow the `SD-NAME` syntax 
(`1*32PRINTUSASCII` with additional character restrictions).
    
    This change enforces these constraints when rendering structured data 
parameters:
    
    * Invalid characters are replaced with `?`.
    * Parameter names are truncated to a maximum of **32 characters**.
    * If sanitization results in an empty name, `?` is used instead.
    
    This ensures that generated structured data complies with the RFC 5424 
grammar for `PARAM-NAME`.
    
    Co-authored-by: Volkan Yazıcı <[email protected]>
---
 .../log4j/core/layout/Rfc5424LayoutTest.java       | 69 +++++++++++++++++++++-
 .../logging/log4j/core/layout/Rfc5424Layout.java   | 64 +++++++++++++++++++-
 src/changelog/.2.x.x/rfc5424-sd-param.xml          | 11 ++++
 3 files changed, 138 insertions(+), 6 deletions(-)

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..ec83dba175 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,19 @@ 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.List;
 import java.util.Locale;
+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 +47,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 +88,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\"]";
@@ -792,4 +806,53 @@ class Rfc5424LayoutTest {
         final Rfc5424Layout layout = Rfc5424Layout.newBuilder().build();
         assertThat(layout.getLocalHostName()).isEqualTo(fqdn);
     }
+
+    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/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..7d9c54c17b 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
@@ -96,6 +96,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;
@@ -579,14 +581,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();
+        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 != ']' && c != 
'"';
+    }
+
+    private String escapeParamValue(final String value) {
         StringBuilder output = null;
         for (int i = 0; i < value.length(); i++) {
             final char cur = value.charAt(i);
diff --git a/src/changelog/.2.x.x/rfc5424-sd-param.xml 
b/src/changelog/.2.x.x/rfc5424-sd-param.xml
new file mode 100644
index 0000000000..7aa963491c
--- /dev/null
+++ b/src/changelog/.2.x.x/rfc5424-sd-param.xml
@@ -0,0 +1,11 @@
+<?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">
+    <description format="asciidoc">
+        Fix sanitization of structured data parameter names in RFC5424 layout.
+    </description>
+</entry>

Reply via email to