This is an automated email from the ASF dual-hosted git repository.
pkarwasz pushed a commit to branch 2.25.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
The following commit(s) were added to refs/heads/2.25.x by this push:
new eb9a0fa429 Rfc5424Layout: Fix sanitization of SD parameter names
(#4073)
eb9a0fa429 is described below
commit eb9a0fa429753ca9050e1800208187447ce0cb60
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Tue Mar 24 23:53:01 2026 +0100
Rfc5424Layout: Fix sanitization of SD parameter names (#4073)
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]>
Co-authored-by: Jan Friedrich <[email protected]>
---
.../log4j/core/layout/Rfc5424LayoutTest.java | 69 +++++++++++++++++++++-
.../logging/log4j/core/layout/Rfc5424Layout.java | 64 +++++++++++++++++++-
src/changelog/.2.x.x/4073_rfc5424-sd-param.xml | 12 ++++
3 files changed, 139 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..bb793b2519 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(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);
diff --git a/src/changelog/.2.x.x/4073_rfc5424-sd-param.xml
b/src/changelog/.2.x.x/4073_rfc5424-sd-param.xml
new file mode 100644
index 0000000000..43428736ac
--- /dev/null
+++ b/src/changelog/.2.x.x/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>