You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by pk...@apache.org on 2026/03/24 17:53:37 UTC

(logging-log4j2) branch fix/2.25.x/rfc5424-sd-param created (now 2e63a37615)

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

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


      at 2e63a37615 Rfc5424Layout: Fix sanitization of SD parameter names

This branch includes the following new commits:

     new 2e63a37615 Rfc5424Layout: Fix sanitization of SD parameter names

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(logging-log4j2) 01/01: Rfc5424Layout: Fix sanitization of SD parameter names

Posted by pk...@apache.org.
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 2e63a3761507c40d657e2d92489a86abf8eb7408
Author: Piotr P. Karwasz <pk...@apache.org>
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ı <vo...@yazi.ci>
---
 .../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..9bfc7cecee 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> paramNameSanitizationCases() {
+        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(name = "[{index}] \"{0}\" -> {1}")
+    @MethodSource("paramNameSanitizationCases")
+    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>