Don't hard-code secrets in constants

Rule ND3114 Security

Matches const string fields, and static readonly string fields whose static constructor only manipulates strings, named like a password, API key, private key, connection string, token, client secret or signing key. A literal secret ships in every build, leaks through decompilation, source control history and crash dumps, and cannot be rotated without a redeploy.

Names that merely designate a setting (PasswordFieldName, ApiKeyHeader, PasswordMinLength, PasswordHash...) or a user-facing text (PasswordTooltip, InvalidSecretMessage...) are excluded. The rule cannot see the literal value: a placeholder like "CHANGE_ME" or an empty string is reported too, suppress such issues.

This is CWE-798 Use of Hard-coded Credentials.

Flagged Code
static class DbSettings {
   const string Password = "P@ssw0rd!";                       // flagged: const secret
   static readonly string ApiKey = "sk-live-" + "4f9c2e1a";   // flagged: only string ops
}
Fixed Code
class DbSettings {
   readonly string _password;
   readonly string _apiKey;
   DbSettings(IConfiguration config) {
      _password = config["Db:Password"];    // user-secrets, environment or key vault
      _apiKey = Environment.GetEnvironmentVariable("API_KEY");
   }
}

How to fix

Read the secret at runtime from a configuration source that is not committed: environment variables, dotnet user-secrets in development, a secret store (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault) or a protected configuration provider in production. Rotate the secret that was committed. Once the field is gone, scan the source history to purge the value.

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>Don't hard-code secrets in constants</Name>
// <Id>ND3114:DontHardCodeSecretsInConstants</Id>
warnif count > 0

from f in Application.Fields
where f.FieldType != null &&
      f.FieldType.FullName == "System.String" &&
     !f.IsGeneratedByCompiler &&

      // Names that designate a secret value...
      f.NameLike(@"(password|passwd|pwd|secret|apikey|api_key|privatekey|private_key|connectionstring|connection_string|accesstoken|access_token|authtoken|auth_token|bearertoken|bearer_token|refreshtoken|refresh_token|clientsecret|client_secret|encryptionkey|encryption_key|signingkey|signing_key|securitykey|security_key|hmackey|hmac_key|masterkey|master_key|credential)\i") &&

      // ...but not a user-facing text mentioning it...
     !f.NameLike(@"(tooltip|message|msg|failure|error|label|title|hint|help|desc|caption|prompt|question|warning|notice|info|text)\i") &&

      // ...nor the name of a setting, header, claim, validation rule or hashed form of it.
      // (key$ matches "ApiKeyKey"-like names that hold the *name* of the setting storing the secret.)
     !f.NameLike(@"(name|names|length|len|regex|pattern|format|header|headers|param|parameter|field|property|prop|placeholder|prefix|suffix|url|uri|path|type|kind|id|hash|hashed|salt|policy|option|options|setting|settings|section|claim|claimtype|scheme|scope|template|required|min|max|count|size|resource|expiration|expiry|lifetime|timeout|attempts|provider|source|env|environment|variable|var|key)$\i")

let cctors = f.ParentType.Methods.Where(m => m.IsClassConstructor).ToArray()
// const: the value is a literal for sure.
// static readonly: the value is a literal unless the static constructor calls anything but String members.
where f.IsLiteral ||
     (f.IsStatic && f.IsInitOnly && cctors.Length > 0 &&
      cctors.All(cctor => !cctor.MethodsCalled.Any(mc => mc.ParentType.FullName != "System.String")))

select new {
  f,
  IsConst = f.IsLiteral,
  Debt = 30.ToMinutes().ToDebt(),
  Severity = Severity.High
}

//<Expl>
// The {1?constant:static read-only} field *{0}* is named like a secret and holds a
// literal value baked into the assembly. Anyone with the binary (or the
// source repository) can read it.
//</Expl>

//<Desc>
// Matches *const* string fields, and *static readonly* string fields whose
// static constructor only manipulates strings, named like a password,
// API key, private key, connection string, token, client secret or
// signing key. A literal secret ships in every build, leaks through
// decompilation, source control history and crash dumps, and cannot be
// rotated without a redeploy.
//
// Names that merely designate a setting (*PasswordFieldName*,
// *ApiKeyHeader*, *PasswordMinLength*, *PasswordHash*...) or a user-facing
// text (*PasswordTooltip*, *InvalidSecretMessage*...) are excluded.
// The rule cannot see the literal value: a placeholder like *"CHANGE_ME"*
// or an empty string is reported too, suppress such issues.
//
// This is CWE-798 *Use of Hard-coded Credentials*.
//
// **Flagged Code**
// ```csharp
// static class DbSettings {
//    const string Password = "P@ssw0rd!";                       // flagged: const secret
//    static readonly string ApiKey = "sk-live-" + "4f9c2e1a";   // flagged: only string ops
// }
// ```
//
// **Fixed Code**
// ```csharp
// class DbSettings {
//    readonly string _password;
//    readonly string _apiKey;
//    DbSettings(IConfiguration config) {
//       _password = config["Db:Password"];    // user-secrets, environment or key vault
//       _apiKey = Environment.GetEnvironmentVariable("API_KEY");
//    }
// }
// ```
//</Desc>

//<HowToFix>
// Read the secret at runtime from a configuration source that is not
// committed: environment variables, *dotnet user-secrets* in development,
// a secret store (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault)
// or a protected configuration provider in production. Rotate the secret
// that was committed. Once the field is gone, scan the source history to
// purge the value.
//</HowToFix>

Security: all 39 items