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 e80b3810acb3a2c9cb9788dab1ef1b9e4aedcf42
Author: Jan Friedrich <[email protected]>
AuthorDate: Mon Aug 17 21:54:18 2026 +0200

    redact the password when reporting a failed database connection
    
    InitializeDatabaseConnection named the resolved connection string in full
    when it could not open the connection, and the documented examples embed
    Password=... The message goes through the ErrorHandler, so it is what an
    operator sees while diagnosing exactly this failure.
    
    Password-bearing keywords are now replaced with *****. The rest of the
    connection string is kept, so the message stays useful for spotting a typo
    in the server name or catalog. If the string cannot be parsed - likely,
    given that it just failed to connect - all of it is redacted.
    
    This matters more since appender errors became visible without
    log4net.Internal.Debug: the password would otherwise have reached stderr in
    a default configuration.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 ...redact-password-in-connection-string-errors.xml | 13 +++++
 .../Appender/AdoNet/Log4NetConnection.cs           | 14 +++++-
 src/log4net.Tests/Appender/AdoNetAppenderTest.cs   | 40 ++++++++++++++++
 src/log4net/Appender/AdoNetAppender.cs             | 56 +++++++++++++++++++++-
 4 files changed, 121 insertions(+), 2 deletions(-)

diff --git 
a/src/changelog/3.4.0/309-redact-password-in-connection-string-errors.xml 
b/src/changelog/3.4.0/309-redact-password-in-connection-string-errors.xml
new file mode 100644
index 00000000..bcc07ebb
--- /dev/null
+++ b/src/changelog/3.4.0/309-redact-password-in-connection-string-errors.xml
@@ -0,0 +1,13 @@
+<?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">
+    stop `AdoNetAppender` from repeating the password when it reports a 
connection it could not
+ open. The message named the resolved connection string in full, and the 
documented examples embed
+ `Password=...` (CWE-532). Password-bearing keywords are now replaced with 
`*****`, while the rest
+ of the connection string is kept so the message stays useful (audit 
1231d72-f018)
+  </description>
+</entry>
diff --git a/src/log4net.Tests/Appender/AdoNet/Log4NetConnection.cs 
b/src/log4net.Tests/Appender/AdoNet/Log4NetConnection.cs
index e11411b0..43364c51 100644
--- a/src/log4net.Tests/Appender/AdoNet/Log4NetConnection.cs
+++ b/src/log4net.Tests/Appender/AdoNet/Log4NetConnection.cs
@@ -44,7 +44,19 @@ internal sealed class Log4NetConnection : IDbConnection
 
   public IDbCommand CreateCommand() => new Log4NetCommand();
 
-  public void Open() => _open = true;
+  public void Open()
+  {
+    if (FailOnOpen)
+    {
+      throw new InvalidOperationException("Simulated failure to open the 
connection");
+    }
+    _open = true;
+  }
+
+  /// <summary>
+  /// When set, <see cref="Open"/> throws, simulating a connection that cannot 
be established.
+  /// </summary>
+  public static bool FailOnOpen { get; set; }
 
   public static Log4NetConnection? MostRecentInstance { get; private set; }
 
diff --git a/src/log4net.Tests/Appender/AdoNetAppenderTest.cs 
b/src/log4net.Tests/Appender/AdoNetAppenderTest.cs
index a07744c8..3e391586 100644
--- a/src/log4net.Tests/Appender/AdoNetAppenderTest.cs
+++ b/src/log4net.Tests/Appender/AdoNetAppenderTest.cs
@@ -265,6 +265,46 @@ public void BufferingWebsiteExample()
     Assert.That(param.Value, Is.Empty);
   }
 
+  /// <summary>
+  /// The message reporting a failed connection must not repeat the password 
from the connection
+  /// string. The appender reports the failure through its ErrorHandler, so 
this message is what
+  /// an operator sees on stderr in a default configuration.
+  /// </summary>
+  [Test]
+  [NonParallelizable]
+  public void FailedConnectionDoesNotReportThePassword()
+  {
+    const string password = "H0rseBatteryStaple";
+    List<LogLog> messages = [];
+    try
+    {
+      Log4NetConnection.FailOnOpen = true;
+      LogLog.ExecuteWithoutEmittingInternalMessages(() =>
+      {
+        using LogLog.LogReceivedAdapter _ = new(messages);
+        AdoNetAppender adoNetAppender = new()
+        {
+          BufferSize = -1,
+          ConnectionType = typeof(Log4NetConnection).AssemblyQualifiedName!,
+          ConnectionString = $"data source=someserver;initial 
catalog=somedb;User ID=someuser;Password={password}",
+          CommandText = "INSERT INTO Log ([Message]) VALUES (@message)"
+        };
+        adoNetAppender.ActivateOptions();
+      });
+
+      string reported = string.Join(Environment.NewLine, messages.ConvertAll(m 
=> m.Message));
+
+      Assert.That(reported, Does.Not.Contain(password));
+      Assert.That(reported, Does.Contain("Could not open database 
connection"));
+      // The rest of the connection string survives, so the message stays 
useful for diagnosis.
+      Assert.That(reported, Does.Contain("someserver"));
+    }
+    finally
+    {
+      Log4NetConnection.FailOnOpen = false;
+    }
+  }
+
   /// <summary>
   /// Without CommandText the rendered Layout is executed as the SQL 
statement, which is open
   /// to SQL injection from logged content. Activation has to say so.
diff --git a/src/log4net/Appender/AdoNetAppender.cs 
b/src/log4net/Appender/AdoNetAppender.cs
index 0faf995e..b4cd2209 100644
--- a/src/log4net/Appender/AdoNetAppender.cs
+++ b/src/log4net/Appender/AdoNetAppender.cs
@@ -21,6 +21,7 @@
 using System.Collections.Generic;
 using System.Configuration;
 using System.Data;
+using System.Data.Common;
 using System.IO;
 
 using log4net.Util;
@@ -788,12 +789,65 @@ private void InitializeDatabaseConnection()
     catch (Exception e) when (!e.IsFatal())
     {
       // Sadly, your connection string is bad.
-      ErrorHandler.Error($"Could not open database connection 
[{resolvedConnectionString}]. Connection string context 
[{connectionStringContext}].", e);
+      ErrorHandler.Error($"Could not open database connection 
[{RedactConnectionString(resolvedConnectionString)}]. Connection string context 
[{connectionStringContext}].", e);
 
       Connection = null;
     }
   }
 
+  /// <summary>
+  /// Replaces the values of password-bearing keywords in a connection string 
with
+  /// <see cref="RedactedValue"/>, so that it can be named in a diagnostic 
message.
+  /// </summary>
+  /// <param name="connectionString">The connection string to redact.</param>
+  /// <returns>
+  /// The connection string with every password value replaced, or <see 
cref="RedactedValue"/> if it
+  /// could not be parsed.
+  /// </returns>
+  private static string RedactConnectionString(string connectionString)
+  {
+    if (string.IsNullOrEmpty(connectionString))
+    {
+      return connectionString;
+    }
+
+    try
+    {
+      DbConnectionStringBuilder builder = new() { ConnectionString = 
connectionString };
+
+      List<string> keys = [];
+      foreach (string key in builder.Keys)
+      {
+        keys.Add(key);
+      }
+
+      foreach (string key in keys)
+      {
+        // Providers spell the secret differently - Password, PWD, User 
Password - so match on
+        // the keyword rather than on a fixed list.
+        if (key.IndexOf("password", StringComparison.OrdinalIgnoreCase) >= 0
+            || key.Equals("pwd", StringComparison.OrdinalIgnoreCase))
+        {
+          builder[key] = RedactedValue;
+        }
+      }
+
+      return builder.ConnectionString;
+    }
+    catch (Exception e) when (!e.IsFatal())
+    {
+      // The connection string could not be parsed - which is likely, given 
that it just failed
+      // to connect - so redact all of it rather than risk echoing a password.
+      LogLog.Debug(_declaringType, "Could not parse the connection string in 
order to redact it", e);
+      return RedactedValue;
+    }
+  }
+
+  /// <summary>
+  /// Stands in for a password in diagnostic messages.
+  /// </summary>
+  private const string RedactedValue = "*****";
+
   /// <summary>
   /// Cleanup the existing connection.
   /// </summary>

Reply via email to