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

FreeAndNil pushed a commit to branch Feature/security-audit-hardening
in repository https://gitbox.apache.org/repos/asf/logging-log4net.git

commit 46582e5204e004bf26569242597acd8896752919
Author: Jan Friedrich <[email protected]>
AuthorDate: Mon Aug 17 22:30:49 2026 +0200

    report a RemoteSyslogAppender Identity that would split the record
    
    The Identity becomes the TAG of the syslog record and was appended
    verbatim, two lines before the message part goes through AppendMessage's
    filtering. A carriage return or line feed in the TAG ends the record, so the
    text after it is read as a record of its own with its own facility and
    severity.
    
    The TAG is a structural identifier and is expected to be a constant rather
    than a pattern rendering event data, so a malformed one is a configuration
    error. It is now reported through the ErrorHandler instead of being repaired
    quietly.
    
    The control characters are removed rather than the event being dropped. An
    Identity pattern that does render event data would otherwise give control
    over whether a record survives at all.
    
    Only control characters are removed. Identity defaults to the application
    friendly name, which may contain a space, and a space cannot split the
    record.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 .../3.4.0/309-report-malformed-syslog-identity.xml | 15 +++++
 .../Appender/RemoteSyslogAppenderTest.cs           | 71 ++++++++++++++++++++--
 src/log4net/Appender/RemoteSyslogAppender.cs       | 60 +++++++++++++++++-
 3 files changed, 140 insertions(+), 6 deletions(-)

diff --git a/src/changelog/3.4.0/309-report-malformed-syslog-identity.xml 
b/src/changelog/3.4.0/309-report-malformed-syslog-identity.xml
new file mode 100644
index 00000000..6c57d4f6
--- /dev/null
+++ b/src/changelog/3.4.0/309-report-malformed-syslog-identity.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       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="fixed">
+  <issue id="309" link="https://github.com/apache/logging-log4net/pull/309"/>
+  <description format="asciidoc">
+    report a `RemoteSyslogAppender` `Identity` that renders control 
characters, and remove them, so
+ that it cannot split the record. The identity becomes the TAG of the syslog 
record and was appended
+ verbatim, while the message part is filtered, so a line feed in it let the 
text that followed be
+ read as a record of its own with an attacker chosen facility and severity. 
The TAG is a structural
+ identifier and expected to be a constant, so this is reported as the 
configuration error it is
+ rather than being repaired quietly (audit 1231d72-f016)
+  </description>
+</entry>
diff --git a/src/log4net.Tests/Appender/RemoteSyslogAppenderTest.cs 
b/src/log4net.Tests/Appender/RemoteSyslogAppenderTest.cs
index 99b9c309..a9f6ed44 100644
--- a/src/log4net.Tests/Appender/RemoteSyslogAppenderTest.cs
+++ b/src/log4net.Tests/Appender/RemoteSyslogAppenderTest.cs
@@ -26,6 +26,7 @@
 using log4net.Core;
 using log4net.Layout;
 using log4net.Tests.Appender.Internal;
+using log4net.Util;
 using NUnit.Framework;
 
 namespace log4net.Tests.Appender;
@@ -117,15 +118,75 @@ public void RemoteSyslogNewLineHandlingSplitTest()
     Assert.That(Encoding.ASCII.GetString(sentBytes[1]), 
Is.EqualTo(expectedData1));
   }
   
+  /// <summary>
+  /// The Identity becomes the TAG of the record. A control character in it 
would end the record,
+  /// so that the rest is read as a second record with its own facility and 
severity.
+  /// </summary>
+  [Test]
+  public void IdentityCannotSplitTheRecord()
+  {
+    List<byte[]> sentBytes = [];
+    // The malformed Identity is reported, which is expected here and should 
not clutter the output.
+    LogLog.ExecuteWithoutEmittingInternalMessages(
+      () => sentBytes = ExecuteAppend("Test message", identity: 
"app\r\n<34>sshd"));
+
+    Assert.That(sentBytes, Has.Count.EqualTo(1));
+    const string expectedData = "<14>app<34>sshd: INFO  - Test message";
+    Assert.That(Encoding.ASCII.GetString(sentBytes[0]), 
Is.EqualTo(expectedData));
+  }
+
+  /// <summary>
+  /// Removing the characters is not enough on its own: a malformed structural 
identifier is a
+  /// configuration error and has to be reported rather than quietly repaired.
+  /// </summary>
+  [Test]
+  [NonParallelizable]
+  public void IdentityWithControlCharactersIsReported()
+  {
+    List<LogLog> messages = [];
+    LogLog.ExecuteWithoutEmittingInternalMessages(() =>
+    {
+      using LogLog.LogReceivedAdapter _ = new(messages);
+      ExecuteAppend("Test message", identity: "app\r\n<34>sshd");
+    });
+
+    Assert.That(messages.ConvertAll(m => m.Message),
+      Has.Some.Contains("Identity of appender"));
+  }
+
+  /// <summary>
+  /// An Identity without control characters has to reach the record 
untouched, including a space,
+  /// which the application friendly name used by default may well contain.
+  /// </summary>
+  [Test]
+  [NonParallelizable]
+  public void IdentityWithoutControlCharactersIsUnchangedAndNotReported()
+  {
+    List<LogLog> messages = [];
+    List<byte[]> sentBytes = [];
+    LogLog.ExecuteWithoutEmittingInternalMessages(() =>
+    {
+      using LogLog.LogReceivedAdapter _ = new(messages);
+      sentBytes = ExecuteAppend("Test message", identity: "My App");
+    });
+
+    Assert.That(sentBytes, Has.Count.EqualTo(1));
+    const string expectedData = "<14>My App: INFO  - Test message";
+    Assert.That(Encoding.ASCII.GetString(sentBytes[0]), 
Is.EqualTo(expectedData));
+    Assert.That(messages.ConvertAll(m => m.Message), 
Has.None.Contains("Identity of appender"));
+  }
+
   private static List<byte[]> ExecuteAppend(string message,
-    RemoteSyslogAppender.SyslogNewLineHandling newLineHandling = default)
+    RemoteSyslogAppender.SyslogNewLineHandling newLineHandling = default,
+    string? identity = null)
   {
     System.Net.IPAddress ipAddress = new([127, 0, 0, 1]);
-    RemoteAppender appender = new() 
-    { 
-      RemoteAddress = ipAddress, 
+    RemoteAppender appender = new()
+    {
+      RemoteAddress = ipAddress,
       Layout = new PatternLayout("%-5level - %message"),
-      NewLineHandling = newLineHandling
+      NewLineHandling = newLineHandling,
+      Identity = identity is null ? null : new PatternLayout(identity)
     };
     appender.ActivateOptions();
     LoggingEvent loggingEvent = new(new()
diff --git a/src/log4net/Appender/RemoteSyslogAppender.cs 
b/src/log4net/Appender/RemoteSyslogAppender.cs
index 8b5b21ea..3d30c526 100644
--- a/src/log4net/Appender/RemoteSyslogAppender.cs
+++ b/src/log4net/Appender/RemoteSyslogAppender.cs
@@ -364,7 +364,7 @@ protected override void Append(LoggingEvent loggingEvent)
       int priority = GeneratePriority(Facility, 
GetSeverity(loggingEvent.Level));
 
       // Identity
-      string? identity = Identity?.Format(loggingEvent) ?? loggingEvent.Domain;
+      string? identity = ValidateIdentity(Identity?.Format(loggingEvent) ?? 
loggingEvent.Domain);
 
       // Message. The message goes after the tag/identity
       string message = RenderLoggingEvent(loggingEvent);
@@ -400,6 +400,64 @@ protected override void Append(LoggingEvent loggingEvent)
     }
   }
 
+  /// <summary>
+  /// Checks that <paramref name="identity"/> is usable as the TAG part of a 
syslog record, and
+  /// reports it through the <see cref="AppenderSkeleton.ErrorHandler"/> when 
it is not.
+  /// </summary>
+  /// <param name="identity">The formatted <see cref="Identity"/>.</param>
+  /// <returns>
+  /// The identity, with any control character removed.
+  /// </returns>
+  /// <remarks>
+  /// <para>
+  /// The TAG is a structural identifier and therefore expected to be a 
constant chosen by the
+  /// developer or operator, not something derived from a logging event. A 
malformed one is a
+  /// configuration error rather than untrusted input, so it is reported 
instead of being altered
+  /// silently: a carriage return or line feed in the TAG would split the 
record and let the text
+  /// after it be read as a second record with its own facility and severity.
+  /// </para>
+  /// <para>
+  /// The offending characters are removed rather than the event being 
dropped, so that an
+  /// <see cref="Identity"/> pattern which does contain event data cannot be 
used to suppress
+  /// records.
+  /// </para>
+  /// </remarks>
+  private string? ValidateIdentity(string? identity)
+  {
+    if (identity is null)
+    {
+      return null;
+    }
+
+    StringBuilder? sanitized = null;
+    for (int i = 0; i < identity.Length; i++)
+    {
+      // Control characters only. A carriage return or line feed ends the 
record, so the text
+      // after it is read as a record of its own. Printable characters are 
left alone, including
+      // the space that an application friendly name may contain, because they 
cannot break the
+      // record apart.
+      if (identity[i] is >= ' ' and not (char)127)
+      {
+        sanitized?.Append(identity[i]);
+      }
+      else
+      {
+        sanitized ??= new StringBuilder(identity.Length).Append(identity, 0, 
i);
+      }
+    }
+
+    if (sanitized is null)
+    {
+      return identity;
+    }
+
+    ErrorHandler.Error(
+      $"The Identity of appender [{Name}] rendered control characters, which 
would have split the syslog record, and they were removed. "
+      + "Identity is a structural identifier and is expected to be a constant 
rather than a pattern that renders logging event data.");
+
+    return sanitized.ToString();
+  }
+
   /// <summary>
   /// Appends the rendered message to the buffer
   /// </summary>

Reply via email to