Skip to content

CWE-522: Insufficiently Protected Credentials - C# / .NET

Overview

Insufficiently protected credentials in C# / ASP.NET Core most commonly manifest as connection strings, API keys, or passwords hardcoded in appsettings.json or another configuration file and committed to source control. When credentials are embedded in code or tracked files, any developer with repository access - or any attacker who gains access to the repository - can extract and use them.

Password storage in databases presents a second aspect: storing passwords in plaintext or hashing them with a fast cryptographic hash (MD5, SHA-1, SHA-256) allows an attacker who breaches the database to recover most passwords within hours using precomputed tables or GPU-accelerated cracking. Correct password storage requires a slow, work-factor-adjustable algorithm designed specifically for passwords.

Primary Defence: Store secrets in Azure Key Vault, AWS Secrets Manager, or .NET User Secrets (development only), and read them through IConfiguration. An environment variable is a way for the deployment platform to hand the process a secret at start-up, not a place to keep one - see CWE-526. Hash passwords with BCrypt (BCrypt.Net-Next) or PBKDF2 (Rfc2898DeriveBytes). Never commit secrets to version control.

Common Vulnerable Patterns

Hardcoded Connection String in appsettings.json

// VULNERABLE - appsettings.json committed to Git
{
  "ConnectionStrings": {
    "Default": "Server=prod.db.example.com;Database=appdb;User=app_user;Password=<redacted-production-password>"
  },
  "ApiKeys": {
    "PaymentProvider": "<redacted-production-api-key>"
  }
}

Why this is vulnerable:

  • Any developer who clones the repository, any CI/CD system, and any cloud service with access to the artifact obtains the production database password and API key. The secret also stays in Git history after the file is changed.

Hardcoded Credentials in Source Code

// VULNERABLE - credentials visible to anyone with repository access
public class EmailService
{
    private const string SmtpPassword = "<redacted-smtp-password>";
    private const string SmtpUser = "noreply@example.com";

    public void SendEmail(string to, string subject, string body)
    {
        using var client = new SmtpClient("smtp.example.com", 587);
        client.Credentials = new NetworkCredential(SmtpUser, SmtpPassword);
        // ...
    }
}

Why this is vulnerable:

  • Constants compiled into the assembly can be extracted with a simple decompiler. The credential also appears in every Git commit that touches this file.

Passwords Stored with SHA-256 (Fast Hash)

// VULNERABLE - fast hash is inappropriate for passwords
public static string HashPassword(string password)
{
    using var sha = SHA256.Create();
    byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(password));
    return Convert.ToHexString(hash);
}

Why this is vulnerable:

  • SHA-256 is designed for speed; it can be computed billions of times per second on a GPU. A database breach exposes all passwords to offline brute-force attacks. SHA-256 also incorporates no salt, enabling rainbow table attacks.

Secure Patterns

Secrets from Environment Variables and User Secrets

// Program.cs - CreateBuilder already registers environment variables, and User
// Secrets when the environment is Development. Nothing further is needed to read
// secrets from either; what changes is that appsettings.json stops carrying them.
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();
// SECURE - inject IConfiguration; secrets sourced at runtime, not compile-time
public class OrderService
{
    private readonly string _connectionString;

    public OrderService(IConfiguration config)
    {
        _connectionString = config.GetConnectionString("Default")
            ?? throw new InvalidOperationException("Connection string 'Default' is not configured.");
    }
}
# The environment variable name is derived from the configuration key, with `__`
# for each `:`. GetConnectionString("Default") reads ConnectionStrings:Default,
# so a variable named DATABASE_URL is not read and the constructor above throws.
export ConnectionStrings__Default='Server=db;Database=appdb;User Id=app;Password=...'
export ApiKeys__PaymentProvider='...'

# Development: stored outside the project tree, in the user profile
dotnet user-secrets set "ConnectionStrings:Default" "Server=localhost;..."

Why this works:

  • Environment variables are set at deploy time, not checked into source control. dotnet user-secrets stores development secrets outside the project directory, in a user-scoped location that is never committed. IConfiguration abstracts the source so no code change is needed between environments.
  • Adding these providers again by hand is not only redundant, it reorders precedence: CreateBuilder puts command-line arguments last so they win, and a trailing AddEnvironmentVariables() moves the environment above them. An explicit AddUserSecrets<Program>() has a second effect - CreateBuilder adds that provider only when the environment is Development, so calling it directly loads the developer's secrets file in every environment.

Azure Key Vault Integration (Production)

// SECURE - secrets stored in Azure Key Vault; accessed via managed identity
if (builder.Environment.IsProduction())
{
    var keyVaultUri = new Uri(builder.Configuration["KeyVaultUri"]!);
    builder.Configuration.AddAzureKeyVault(keyVaultUri, new DefaultAzureCredential());
}

Why this works:

  • Managed identity eliminates the need for any stored credential to access the vault - the Microsoft Entra identity of the VM/App Service is used. Secrets are never stored in the application's configuration files.

Password Hashing with BCrypt

dotnet add package BCrypt.Net-Next
// The package's class is BCrypt.Net.BCrypt, so `using BCrypt.Net;` followed by a
// bare `BCrypt.HashPassword(...)` does not compile - the compiler resolves BCrypt
// to the namespace and reports CS0234. Qualify it, or alias it as here.
using BC = BCrypt.Net.BCrypt;

// SECURE - adaptive work factor; automatically salted
public static class PasswordHelper
{
    private const int WorkFactor = 12; // Adjust upward as hardware improves

    public static string HashPassword(string plainPassword)
        => BC.HashPassword(plainPassword, workFactor: WorkFactor);

    public static bool VerifyPassword(string plainPassword, string storedHash)
        => BC.Verify(plainPassword, storedHash);
}

// Usage
var hash = PasswordHelper.HashPassword(registrationRequest.Password);
// Store hash in database

var isValid = PasswordHelper.VerifyPassword(loginRequest.Password, user.PasswordHash);

Why this works:

  • BCrypt is deliberately slow and salts each hash automatically. A workFactor of 12 means the algorithm runs 2^12 (4096) iterations for each guess, so the offline brute-force that breaks a SHA-256 table stays expensive. Raise the factor as hardware improves.

Testing

  • Normal input: start the application with secrets supplied by environment variables, user secrets, or Key Vault and confirm dependent services connect.
  • Boundary input: test missing, malformed, and rotated credentials to confirm startup or health checks fail closed with useful diagnostics.
  • Malicious input: search the working tree and recent Git history for known test secrets; rotate anything that was previously committed.
  • Inspect a stored hash directly: it should begin with $2a$ for BCrypt and differ between two hashes of the same password.
  • Legacy SHA/MD5 hashes cannot be converted - you do not have the passwords. Verify against the old format and rehash with BCrypt on the next successful login, retiring the old path once the population has drained.
  • Add appsettings.*.json overrides and .env to .gitignore, and scan history before concluding a committed secret is gone.

Additional Resources