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, committed to source control, or stored as plaintext in configuration files. 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 environment variables, Azure Key Vault, AWS Secrets Manager, or .NET User Secrets (development only). 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 is also preserved in Git history forever, even if the file is later 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 does not incorporate a salt by default, enabling rainbow table attacks.
Secure Patterns
Secrets from Environment Variables and User Secrets
// Program.cs - load secrets from environment variables (production)
// and User Secrets (development); never from appsettings.json
var builder = WebApplication.CreateBuilder(args);
builder.Configuration
.AddEnvironmentVariables() // DATABASE_URL, STRIPE_KEY, etc.
.AddUserSecrets<Program>(optional: true); // Development: dotnet user-secrets set "Key" "Val"
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.");
}
}
Why this works:
- Environment variables are set at deploy time, not checked into source control.
dotnet user-secretsstores development secrets outside the project directory, in a user-scoped location that is never committed.IConfigurationabstracts the source so no code change is needed between environments.
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 Azure AD identity of the VM/App Service is used. Secrets are never stored in the application's configuration files.
Password Hashing with BCrypt
using BCrypt.Net;
// 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)
=> BCrypt.HashPassword(plainPassword, workFactor: WorkFactor);
public static bool VerifyPassword(string plainPassword, string storedHash)
=> BCrypt.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 a slow, work-factor-adjustable algorithm that automatically salts each hash. A
workFactorof 12 means the algorithm runs 2^12 (4096) iterations, making brute-force prohibitively slow. The work factor can be increased over time as hardware improves.
Remediation Steps
Locate the Finding
- Source: Source code files,
appsettings.json,web.config,*.envfiles committed to Git - Sink: Hardcoded strings containing passwords, API keys, connection strings;
MD5/SHA1/SHA256for password hashing
Apply the Fix
- PRIORITY 1: Move all credentials out of source files and into environment variables or a vault
- PRIORITY 2: Add
appsettings.*.jsonoverrides and.envto.gitignore; rungit secretor similar to scan history - PRIORITY 3: Migrate password storage to BCrypt: re-hash on next successful login if you have existing SHA/MD5 hashes
Verify the Fix
- Confirm
appsettings.jsonno longer contains secrets; check Git history for prior commits - Register a test user, inspect the stored hash - it should start with
$2a$(BCrypt) and vary between runs - Rescan with the security scanner to confirm the finding is resolved
Check for Similar Issues
Search for: Password=, ApiKey=, secret, MD5.Create(), SHA1.Create(), SHA256.Create() used for passwords
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.