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>
