Review secrets passed to logging methods

Rule ND3127 Security

Matches methods that call a logging API (ILogger / LoggerExtensions, Serilog, NLog, log4net, Trace, Debug, Console) and, in the same body, read a string field or property getter named like a password, API key, private key, token, client secret, connection string or credential. Logs are stored longer, replicated more widely and protected less than the secret store; a connection string or bearer token in a log line is one of the most frequent real-world credential leaks.

This is CWE-532 Insertion of Sensitive Information into Log File (review: the rule cannot tell whether the value is part of the message).

Flagged Code
private readonly string _connectionString;

void OpenDatabase(ILogger logger) {
   logger.LogInformation("Opening {Db}", _connectionString);   // flagged: secret logged
   using var connection = new SqlConnection(_connectionString);
   connection.Open();
}
Fixed Code
private readonly string _connectionString;

void OpenDatabase(ILogger logger) {
   logger.LogInformation("Opening database {Db}", DatabaseName);   // non-secret value
   using var connection = CreateConnection();
   connection.Open();
}

SqlConnection CreateConnection() => new SqlConnection(_connectionString);   // logs nothing

How to fix

Never pass the secret to the logger: log its presence or a redacted form (last 4 characters, a hash), and use a destructuring policy that masks sensitive properties when logging whole objects. Suppress the issue when the secret is read for another purpose in the method and demonstrably not part of the log call.

CQLinq source

This rule is written in CQLinq, C# LINQ over the NDepend code model. It ships with every new NDepend project: you can read it, adapt it, or use it as a starting point for your own custom rules.

// <Name>Review secrets passed to logging methods</Name>
// <Id>ND3127:ReviewSecretsPassedToLoggingMethods</Id>
warnif count > 0

// Third-party members are part of the code model only when the application uses them.
let loggingMembers = ThirdParty.Methods.Where(m =>
      (m.ParentType.FullNameEqualsAny(
          "Microsoft.Extensions.Logging.LoggerExtensions", "Microsoft.Extensions.Logging.ILogger",
          "Serilog.ILogger", "Serilog.Log", "NLog.Logger", "NLog.ILogger", "log4net.ILog", "Common.Logging.ILog",
          "System.Diagnostics.Trace", "System.Diagnostics.Debug", "System.Diagnostics.TraceSource", "System.Diagnostics.EventLog") &&
       m.SimpleName.StartsWithAny("Log", "Write", "Trace", "Debug", "Info", "Warn", "Error", "Fatal", "Verbose", "Critical")) ||
      (m.ParentType.FullName == "System.Console" && m.SimpleName.StartsWith("Write")))
   .ToHashSetEx()
where loggingMembers.Any()

// Fields and property getters whose name designates a secret (hashed forms and setting names excluded).
let isSecretName = new Func<string, bool>(n =>
      n.ToLower().ContainsAny("password", "passwd", "pwd", "secret", "apikey", "api_key", "privatekey", "private_key",
                              "accesstoken", "access_token", "authtoken", "auth_token", "refreshtoken", "refresh_token",
                              "clientsecret", "client_secret", "connectionstring", "connection_string", "credential") &&
     !n.ToLower().ContainsAny("hash", "salt", "length", "policy", "regex", "pattern", "name", "header", "expir", "required", "attempt", "changed", "reset", "enabled", "type", "kind", "count", "provider"))

from m in Application.Methods.UsingAny(loggingMembers)
let secretFieldsRead = m.FieldsUsed.Where(f => !f.IsGeneratedByCompiler && f.FieldType != null && f.FieldType.FullName == "System.String" && isSecretName(f.SimpleName)).ToArray()
let secretGettersCalled = m.MethodsCalled.Where(mc => mc.IsPropertyGetter && mc.ReturnType != null && mc.ReturnType.FullName == "System.String" && isSecretName(mc.SimpleName)).ToArray()
where secretFieldsRead.Length > 0 || secretGettersCalled.Length > 0
select new {
  m,
  secretFieldsRead,
  secretGettersCalled,
  loggingMembersUsed = m.MethodsCalled.Intersect(loggingMembers).ToArray(),
  Debt = 15.ToMinutes().ToDebt(),
  Severity = Severity.Medium
}

//<Expl>
// The *{0}* both logs (through *{3}*) and reads the secret-named value(s)
// {1}{2}. If the secret ends up in the log message, it is exposed to
// everyone who can read the logs, log aggregators and support bundles.
//</Expl>

//<Desc>
// Matches methods that call a logging API (*ILogger* / *LoggerExtensions*,
// Serilog, NLog, log4net, *Trace*, *Debug*, *Console*) and, in the same
// body, read a string field or property getter named like a password, API
// key, private key, token, client secret, connection string or credential.
// Logs are stored longer, replicated more widely and protected less than
// the secret store; a connection string or bearer token in a log line is
// one of the most frequent real-world credential leaks.
//
// This is CWE-532 *Insertion of Sensitive Information into Log File*
// (review: the rule cannot tell whether the value is part of the message).
//
// **Flagged Code**
// ```csharp
// private readonly string _connectionString;
//
// void OpenDatabase(ILogger logger) {
//    logger.LogInformation("Opening {Db}", _connectionString);   // flagged: secret logged
//    using var connection = new SqlConnection(_connectionString);
//    connection.Open();
// }
// ```
//
// **Fixed Code**
// ```csharp
// private readonly string _connectionString;
//
// void OpenDatabase(ILogger logger) {
//    logger.LogInformation("Opening database {Db}", DatabaseName);   // non-secret value
//    using var connection = CreateConnection();
//    connection.Open();
// }
//
// SqlConnection CreateConnection() => new SqlConnection(_connectionString);   // logs nothing
// ```
//</Desc>

//<HowToFix>
// Never pass the secret to the logger: log its presence or a redacted
// form (last 4 characters, a hash), and use a destructuring policy that
// masks sensitive properties when logging whole objects. Suppress the
// issue when the secret is read for another purpose in the method and
// demonstrably not part of the log call.
//</HowToFix>

Security: all 39 items