CWE-316: Cleartext Storage of Sensitive Information in Memory - C#
Overview
Storing sensitive data (passwords, cryptographic keys, tokens) in memory as cleartext in C# exposes it to memory dumps, debuggers, and memory disclosure vulnerabilities. Regular strings are immutable and persist in memory until garbage collected. Use SecureString for passwords, clear arrays explicitly with Array.Clear(), and leverage SafeHandle for unmanaged memory when needed.
Primary Defence: Use char[] for passwords with explicit Array.Clear() in finally blocks (cross-platform compatible), implement AutoCloseable pattern with using statements for automatic cleanup of sensitive data, and use ASP.NET Core's Data Protection API or in-memory encryption for storing keys to prevent cleartext persistence in memory dumps and enable deterministic clearing when data is no longer needed.
Common Vulnerable Patterns
Storing password as String
using System;
// VULNERABLE - String is immutable, persists in memory
public class InsecureAuth
{
private string _password;
public bool Authenticate(string username, string password)
{
// Password stored as immutable string
// Cannot be cleared from memory
_password = password;
bool result = VerifyPassword(username, password);
// Setting to null doesn't clear original string
_password = null;
return result;
}
}
Storing API keys as string fields
// VULNERABLE - API keys persist in heap
public class APIClient
{
private string _apiKey;
private string _apiSecret;
public APIClient(string key, string secret)
{
// Immutable strings - visible in memory dumps
_apiKey = key;
_apiSecret = secret;
}
public async Task<string> MakeRequestAsync(string endpoint)
{
using var client = new HttpClient();
// API key exposed in memory
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", _apiKey);
var response = await client.GetAsync(endpoint);
return await response.Content.ReadAsStringAsync();
}
}
Logging sensitive data
using Microsoft.Extensions.Logging;
// VULNERABLE - Password logged to files
public class LoginService
{
private readonly ILogger<LoginService> _logger;
public void Login(string username, string password)
{
_logger.LogDebug($"Login attempt: {username} with password {password}");
// Password now in log files and log string objects
bool success = Authenticate(username, password);
_logger.LogInformation($"Login result: {success}");
}
}
Not disposing SecureString
using System.Security;
// VULNERABLE - SecureString never disposed
public class PasswordForm
{
public bool Submit(SecureString password)
{
// Use password but never dispose
bool result = Authenticate(password);
// SecureString remains in memory - not cleared
return result;
}
}
Converting SecureString to String
using System;
using System.Runtime.InteropServices;
using System.Security;
// VULNERABLE - Defeats purpose of SecureString
public class PasswordHandler
{
public void ProcessPassword(SecureString securePassword)
{
// Converting to String creates cleartext copy
IntPtr ptr = Marshal.SecureStringToBSTR(securePassword);
try
{
string password = Marshal.PtrToStringBSTR(ptr);
// Now password exists in both SecureString and String
ProcessCredential(password);
// Even if we clear SecureString, string remains
}
finally
{
Marshal.ZeroFreeBSTR(ptr);
}
}
}
Secure Patterns
Using SecureString for passwords
Platform Note Warning:
SecureStringencrypts in memory on Windows (using DPAPI) but only pins memory on Linux/macOS without encryption. For cross-platform applications, consider usingbyte[]orchar[]with explicit clearing instead. Microsoft does not recommendSecureStringfor new code in .NET Core/.NET 5+.
using System;
using System.Runtime.InteropServices;
using System.Security;
public class SecureAuth
{
public bool Authenticate(string username, SecureString password)
{
// SecureString encrypts password in memory (Windows only)
// Must dispose to clear
IntPtr ptr = IntPtr.Zero;
try
{
// Convert to unmanaged memory for use
ptr = Marshal.SecureStringToBSTR(password);
// Use pointer directly, avoid converting to string
bool result = VerifyPasswordPtr(username, ptr);
return result;
}
finally
{
// Always clear unmanaged memory
if (ptr != IntPtr.Zero)
{
Marshal.ZeroFreeBSTR(ptr);
}
}
}
private bool VerifyPasswordPtr(string username, IntPtr passwordPtr)
{
// Work with pointer to avoid creating string
// Compare with stored hash
return true; // Implement actual verification
}
}
Why this works:
SecureStringencrypts data in memory on Windows: Uses DPAPI to prevent cleartext in memory dumps- Unmanaged memory allows secure overwriting: Unlike managed heap, can be zeroed before release
Marshal.ZeroFreeBSTRclears memory securely: Zeros unmanaged memory before releasing itfinallyensures cleanup: Memory cleared even if exceptions occur- Platform limitation: Only encrypts on Windows; Linux/macOS only pins memory without encryption
- Microsoft guidance: No longer recommended for .NET Core/.NET 5+ cross-platform code
Using char[] with explicit clearing
✅ Cross-Platform Recommended: This pattern works identically on all platforms and is recommended by Microsoft for new code.
using System;
public class SecurePasswordHandler
{
public bool AuthenticateWithCharArray(string username, char[] password)
{
try
{
// Use char array for password
byte[] passwordBytes = System.Text.Encoding.UTF8.GetBytes(password);
try
{
// Hash and verify
byte[] hash = HashPassword(passwordBytes);
return CompareHashes(hash, GetStoredHash(username));
}
finally
{
// Clear byte array
Array.Clear(passwordBytes, 0, passwordBytes.Length);
}
}
finally
{
// Always clear char array
Array.Clear(password, 0, password.Length);
}
}
private byte[] HashPassword(byte[] passwordBytes)
{
using var sha256 = System.Security.Cryptography.SHA256.Create();
return sha256.ComputeHash(passwordBytes);
}
}
Why this works:
char[]is mutable and clearable:Array.Clear()zeros memory, unlike immutable strings that persist until GC- Nested
try-finallyensures complete cleanup: Bothchar[]and intermediatebyte[]are zeroed - Minimizes cleartext window: Clearing
byte[]immediately after use reduces exposure time - Cross-platform consistency: Works identically on Windows, Linux, macOS
- Microsoft's current recommendation: Preferred over
SecureStringfor .NET Core/.NET 5+ applications
✅ BEST: Session-based memory encryption for sensitive data
🔒 Advanced Protection: Encrypt sensitive data in RAM with random session key. Data remains encrypted in memory - stronger than cleartext
char[].
How it works (4 layers):
- SecureKeyManager - AES-256-GCM encryption with random nonces
- SecretEncryption - Session key management & auto-cleanup
- ProtectedSecret - High-level API with automatic memory clearing
- Your application code using ProtectedSecret
Key management (automatic):
SecretEncryption.Instanceis a singleton - one instance per application- On first use, generates 32-byte random encryption key automatically
- Key lives in memory for entire app lifetime
- Cleared on app shutdown (handles
ProcessExit,DomainUnload,Ctrl+C) - You never handle the key directly - just call
ProtectInMemory()/UnprotectFromMemory()
Data flow:
- Password arrives as
char[]→ProtectedPasswordencrypts it → stored as encryptedbyte[]in RAM - When needed:
GetPassword()→ temporarily decrypts tochar[]→ use briefly → clear immediately - Result: Password only exists in cleartext for microseconds, rest of time encrypted with random session key
using System;
using System.Security.Cryptography;
using System.Text;
// LAYER 1: Low-level encryption utility
public class SecureKeyManager(byte[] key) : IDisposable
{
// Use explicit standard sizes for AES-GCM
private const int NonceSize = 12; // 96 bits - standard for GCM
private const int TagSize = 16; // 128 bits - standard authentication tag
// Associated data for binding ciphertext to context
private static readonly byte[] DefaultAad = Encoding.UTF8.GetBytes("Dipsy.MemoryProtection:v1");
private byte[]? _keyBytes = InitializeKey(key);
private bool _disposed = false;
private static byte[] InitializeKey(byte[] key)
{
// Store key in byte array (must be 32 bytes for AES-256)
if (key.Length != 32)
{
throw new ArgumentException("Key must be 32 bytes for AES-256-GCM");
}
var keyBytes = new byte[key.Length];
Array.Copy(key, keyBytes, key.Length);
return keyBytes;
}
public byte[] Encrypt(byte[] plaintext)
{
if (_disposed)
{
throw new ObjectDisposedException("SecureKeyManager");
}
// Use AesGcm class for authenticated encryption (.NET Core 3.0+)
using var aesGcm = new AesGcm(_keyBytes!, TagSize);
// Generate random nonce (12 bytes - standard for GCM)
var nonce = new byte[NonceSize];
RandomNumberGenerator.Fill(nonce);
// Allocate space for ciphertext and auth tag
var ciphertext = new byte[plaintext.Length];
var tag = new byte[TagSize];
try
{
// Encrypt with authentication and associated data (AAD)
// AAD binds ciphertext to context, prevents mix-and-match attacks
aesGcm.Encrypt(nonce, plaintext, ciphertext, tag, DefaultAad);
// Return: nonce + tag + ciphertext
var result = new byte[nonce.Length + tag.Length + ciphertext.Length];
Buffer.BlockCopy(nonce, 0, result, 0, nonce.Length);
Buffer.BlockCopy(tag, 0, result, nonce.Length, tag.Length);
Buffer.BlockCopy(ciphertext, 0, result, nonce.Length + tag.Length, ciphertext.Length);
return result;
}
finally
{
// Clear intermediate buffers for defense in depth
Array.Clear(nonce, 0, nonce.Length);
Array.Clear(tag, 0, tag.Length);
Array.Clear(ciphertext, 0, ciphertext.Length);
}
}
public byte[] Decrypt(byte[] encryptedData)
{
if (_disposed)
{
throw new ObjectDisposedException("SecureKeyManager");
}
// Validate minimum length before slicing
int minLength = NonceSize + TagSize;
if (encryptedData == null || encryptedData.Length < minLength)
{
throw new CryptographicException(
$"Encrypted data must be at least {minLength} bytes (nonce + tag). Received: {encryptedData?.Length ?? 0} bytes");
}
// Extract nonce, tag, and ciphertext
var nonce = new byte[NonceSize];
var tag = new byte[TagSize];
var ciphertext = new byte[encryptedData.Length - NonceSize - TagSize];
Buffer.BlockCopy(encryptedData, 0, nonce, 0, NonceSize);
Buffer.BlockCopy(encryptedData, NonceSize, tag, 0, TagSize);
Buffer.BlockCopy(encryptedData, NonceSize + TagSize, ciphertext, 0, ciphertext.Length);
// Decrypt and verify authentication tag
using var aesGcm = new AesGcm(_keyBytes!, TagSize);
var plaintext = new byte[ciphertext.Length];
try
{
// Decrypt with AAD verification - ensures ciphertext hasn't been moved between contexts
aesGcm.Decrypt(nonce, ciphertext, tag, plaintext, DefaultAad);
return plaintext;
}
catch
{
// Clear plaintext buffer on decryption failure (bad tag, etc.)
Array.Clear(plaintext, 0, plaintext.Length);
throw;
}
finally
{
// Clear intermediate buffers for defense in depth
Array.Clear(nonce, 0, nonce.Length);
Array.Clear(tag, 0, tag.Length);
Array.Clear(ciphertext, 0, ciphertext.Length);
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (_keyBytes != null)
{
// Clear key from memory
Array.Clear(_keyBytes, 0, _keyBytes.Length);
_keyBytes = null;
}
_disposed = true;
}
~SecureKeyManager()
{
Dispose(false);
}
}
using System.Security.Cryptography;
using System.Text;
/// <summary>
/// Singleton for encrypting/decrypting sensitive data in memory.
/// The session key is randomly generated on first use and cleared on application shutdown (best-effort).
///
/// LIMITATIONS:
/// - Cleanup hooks are best-effort and won't run on hard termination/crash
/// - Encoding conversions may create transient runtime buffers that can't be reliably wiped in managed environments
/// - Key material is stored in managed memory (subject to GC movement)
/// </summary>
public sealed class SecretEncryption
{
private static readonly Lazy<SecretEncryption> _instance = new(() => new SecretEncryption());
public static SecretEncryption Instance => _instance.Value;
private readonly SecureKeyManager _keyManager;
private volatile bool _disposed = false;
private SecretEncryption()
{
// Generate random session key and create single SecureKeyManager instance
byte[] sessionKey = new byte[32];
RandomNumberGenerator.Fill(sessionKey);
try
{
_keyManager = new SecureKeyManager(sessionKey);
}
finally
{
// Clear the temporary session key array
Array.Clear(sessionKey, 0, sessionKey.Length);
}
// Multiple cleanup hooks for different shutdown scenarios
try
{
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
AppDomain.CurrentDomain.DomainUnload += OnProcessExit;
// For console apps - handle Ctrl+C
Console.CancelKeyPress += OnCancelKeyPress;
}
catch
{
// Event registration failed - still continue
// Key will be cleared by finalizer if needed
}
}
private void OnProcessExit(object? sender, EventArgs e)
{
try
{
Cleanup();
}
catch
{
// Suppress exceptions during shutdown
// Don't prevent other cleanup handlers from running
}
}
private void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e)
{
try
{
Cleanup();
}
catch
{
// Suppress exceptions
}
}
public byte[] ProtectInMemory(char[] secret)
{
if (_disposed)
{
throw new ObjectDisposedException("SecretEncryption");
}
try
{
// Convert char[] to bytes
byte[] secretBytes = Encoding.UTF8.GetBytes(secret);
try
{
// Encrypt using shared session key manager
// No lock needed - SecureKeyManager creates new AesGcm per call
return _keyManager.Encrypt(secretBytes);
}
finally
{
// Clear plaintext secret bytes
Array.Clear(secretBytes, 0, secretBytes.Length);
}
}
finally
{
// Clear original char array
Array.Clear(secret, 0, secret.Length);
}
}
public char[] UnprotectFromMemory(byte[] encryptedData)
{
if (_disposed)
{
throw new ObjectDisposedException("SecretEncryption");
}
// Decrypt using shared session key manager
// No lock needed - SecureKeyManager creates new AesGcm per call
byte[] secretBytes = _keyManager.Decrypt(encryptedData);
try
{
// Decode UTF-8 bytes directly to char[] without creating intermediate string
int charCount = Encoding.UTF8.GetCharCount(secretBytes);
char[] result = new char[charCount];
Encoding.UTF8.GetChars(secretBytes, 0, secretBytes.Length, result, 0);
return result;
}
finally
{
// Clear decrypted bytes
Array.Clear(secretBytes, 0, secretBytes.Length);
}
}
private void Cleanup()
{
if (_disposed) return;
_disposed = true;
_keyManager?.Dispose();
}
}
/// <summary>
/// Stores a secret encrypted in memory. The secret is only decrypted temporarily when accessed via UseSecret callbacks.
/// Use ProtectedSecret.Consume() to create an instance.
/// </summary>
public class ProtectedSecret : IDisposable
{
private byte[]? _encryptedData;
private bool _disposed = false;
/// <summary>
/// Private constructor - use Consume() factory method instead.
/// </summary>
private ProtectedSecret(byte[] encryptedData)
{
_encryptedData = encryptedData;
}
/// <summary>
/// Creates a new ProtectedSecret by consuming and encrypting the provided secret.
/// The input secret array is cleared (zeroed) for security after encryption.
/// </summary>
/// <param name="secret">Secret as char array. This array will be cleared (zeroed) after encryption.</param>
/// <returns>A new ProtectedSecret with the encrypted secret.</returns>
public static ProtectedSecret Consume(char[] secret)
{
byte[] encryptedData = SecretEncryption.Instance.ProtectInMemory(secret);
// Note: secret array is now cleared (all zeros) by ProtectInMemory
return new ProtectedSecret(encryptedData);
}
/// <summary>
/// Safely use the secret within a callback. The secret is automatically cleared after the callback completes.
/// Callers must not copy the secret (e.g., new string(secret) or secret.ToArray()) as copies won't be cleared.
/// </summary>
/// <param name="action">Callback that receives the secret as a ReadOnlySpan. Do not store or copy this span - work with it directly.</param>
/// <exception cref="ObjectDisposedException">Thrown if this ProtectedSecret has been disposed.</exception>
/// <exception cref="ArgumentNullException">Thrown if action is null.</exception>
public void UseSecret(Action<ReadOnlySpan<char>> action)
{
if (_disposed)
{
throw new ObjectDisposedException("ProtectedSecret");
}
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
char[]? tempSecret = null;
try
{
tempSecret = SecretEncryption.Instance.UnprotectFromMemory(_encryptedData!);
// Pass as ReadOnlySpan to prevent caller from modifying
action(tempSecret.AsSpan());
}
finally
{
// Plaintext is auto-cleared after callback; callers must not copy it
if (tempSecret != null)
{
Array.Clear(tempSecret, 0, tempSecret.Length);
}
}
}
/// <summary>
/// Safely use the secret within a callback that returns a result. The secret is automatically cleared after the callback completes.
/// Callers must not copy the secret (e.g., new string(secret) or secret.ToArray()) as copies won't be cleared.
/// </summary>
/// <typeparam name="TResult">The type of result returned by the callback.</typeparam>
/// <param name="func">Callback that receives the secret as a ReadOnlySpan and returns a result. Do not store or copy the span - work with it directly.</param>
/// <returns>The result from the callback.</returns>
/// <exception cref="ObjectDisposedException">Thrown if this ProtectedSecret has been disposed.</exception>
/// <exception cref="ArgumentNullException">Thrown if func is null.</exception>
public TResult UseSecret<TResult>(Func<ReadOnlySpan<char>, TResult> func)
{
if (_disposed)
{
throw new ObjectDisposedException("ProtectedSecret");
}
if (func == null)
{
throw new ArgumentNullException(nameof(func));
}
char[]? tempSecret = null;
try
{
tempSecret = SecretEncryption.Instance.UnprotectFromMemory(_encryptedData!);
// Pass as ReadOnlySpan to prevent caller from modifying
return func(tempSecret.AsSpan());
}
finally
{
// Plaintext is auto-cleared after callback; callers must not copy it
if (tempSecret != null)
{
Array.Clear(tempSecret, 0, tempSecret.Length);
}
}
}
/// <summary>
/// Disposes this ProtectedSecret, clearing the encrypted data from memory.
/// </summary>
public void Dispose()
{
if (_disposed) return;
// Clear encrypted data
if (_encryptedData != null)
{
Array.Clear(_encryptedData, 0, _encryptedData.Length);
_encryptedData = null;
}
_disposed = true;
}
}
// Your application code (usage example)
// Simulate password coming from another source, ready for storage
char[] password = "MySecurePassword123!".ToCharArray();
using var ProtectedSecret = ProtectedSecret.Consume(password);
// password[] is now all zeros - it was consumed!
// Use password safely with automatic cleanup
ProtectedSecret.UseSecret(pwd =>
{
// Use password briefly for authentication
// pwd is a ReadOnlySpan<char> - should not be stored or copied
AuthenticateUser(pwd);
// Plaintext automatically cleared when callback completes!
});
This is also available for download, with additional documentation and example code, from https://github.com/dipsylala/Dipsy.Security.MemoryProtection
Why this works:
- RAM encryption: AES-256-GCM with random nonces encrypts sensitive data in memory - memory dumps show ciphertext, not plaintext
- Four-layer architecture:
SecureKeyManager(AES-GCM encryption),SecretEncryption(singleton session key),ProtectedSecret(high-level API), application code (simple calls) - Session key lifecycle: Randomly generated on first use, lives only in app memory (never persisted), cleared on shutdown via cleanup hooks
- Minimal plaintext exposure:
ReadOnlySpan<char>callback inUseSecret()prevents copying; plaintext exists only microseconds during use, then immediately re-encrypted - AAD binding: Associated Authenticated Data prevents ciphertext mix-and-match attacks across different contexts
ASP.NET Core authentication with secure password handling
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Security;
using System.Threading.Tasks;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
public AuthController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
// Convert password to char array immediately
char[] passwordChars = request.Password.ToCharArray();
try
{
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null)
{
return Unauthorized(new { error = "Invalid credentials" });
}
// ASP.NET Core Identity handles password hashing securely
// PasswordHasher clears sensitive data internally
var result = await _signInManager.CheckPasswordSignInAsync(
user,
request.Password,
lockoutOnFailure: false
);
if (result.Succeeded)
{
return Ok(new { success = true });
}
return Unauthorized(new { error = "Invalid credentials" });
}
finally
{
// Clear password char array
Array.Clear(passwordChars, 0, passwordChars.Length);
}
}
}
public class LoginRequest
{
public string Username { get; set; }
public string Password { get; set; }
}
Why this works:
- ASP.NET Core Identity uses secure hashing: PBKDF2 with HMAC-SHA256 (or bcrypt/Argon2) prevents cleartext storage
- Constant-time comparison prevents timing attacks: Password verification doesn't leak information through timing
char[]withfinallyminimizes cleartext exposure: Password cleared from memory even on exceptions- Generic error messages prevent enumeration: Same message for non-existent users and wrong passwords
- Built-in lockout protection: Setting
lockoutOnFailure: true(production) prevents brute-force attacks
Secure credential storage with SafeHandle
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
public class SecureCredentialHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private SecureCredentialHandle() : base(true) { }
public SecureCredentialHandle(IntPtr preexistingHandle, bool ownsHandle)
: base(ownsHandle)
{
SetHandle(preexistingHandle);
}
protected override bool ReleaseHandle()
{
// Zero memory before freeing
if (handle != IntPtr.Zero)
{
// Get size of allocated memory (store separately)
// Zero the memory
// Free the memory
return true;
}
return false;
}
}
public class SecureCredentialManager : IDisposable
{
private SecureCredentialHandle _credentialHandle;
public void StoreCredential(byte[] credential)
{
// Allocate unmanaged memory
IntPtr ptr = Marshal.AllocHGlobal(credential.Length);
try
{
// Copy credential to unmanaged memory
Marshal.Copy(credential, 0, ptr, credential.Length);
// Wrap in SafeHandle
_credentialHandle = new SecureCredentialHandle(ptr, true);
}
catch
{
// If SafeHandle creation fails, manually free
Marshal.FreeHGlobal(ptr);
throw;
}
finally
{
// Clear original credential
Array.Clear(credential, 0, credential.Length);
}
}
public void Dispose()
{
_credentialHandle?.Dispose();
}
}
Why this works:
SafeHandleensures automatic memory clearing: Zeros and frees unmanaged memory when disposed- Unmanaged memory avoids GC issues: Not subject to garbage collection copying/compaction
- Critical region prevents use-after-free: Memory can't be freed while another thread is using it
- Finalizer provides safety net:
ReleaseHandle()called even ifDispose()not explicitly called - Single controlled copy: Moving to unmanaged memory and clearing managed copy reduces exposure
- Native API compatibility: Unmanaged memory can be passed directly via pointers without string copies
JWT token handling with Data Protection API
using Microsoft.AspNetCore.DataProtection;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
public class SecureJWTHandler : IDisposable
{
private byte[] _secretKey;
private readonly IDataProtector _protector;
private bool _disposed = false;
public SecureJWTHandler(byte[] secretKey, IDataProtectionProvider provider)
{
// Store secret in byte array
_secretKey = new byte[secretKey.Length];
Array.Copy(secretKey, _secretKey, secretKey.Length);
// Use Data Protection API for additional encryption
_protector = provider.CreateProtector("JWTSecrets");
}
public string CreateToken(string userId)
{
if (_disposed)
{
throw new ObjectDisposedException("SecureJWTHandler");
}
var tokenHandler = new JwtSecurityTokenHandler();
var key = new SymmetricSecurityKey(_secretKey);
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
claims: new[] { new Claim(ClaimTypes.NameIdentifier, userId) },
expires: DateTime.UtcNow.AddHours(1),
signingCredentials: credentials
);
return tokenHandler.WriteToken(token);
}
public ClaimsPrincipal ValidateToken(string token)
{
if (_disposed)
{
throw new ObjectDisposedException("SecureJWTHandler");
}
var tokenHandler = new JwtSecurityTokenHandler();
var key = new SymmetricSecurityKey(_secretKey);
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero
};
return tokenHandler.ValidateToken(token, validationParameters, out _);
}
public void Dispose()
{
if (!_disposed)
{
if (_secretKey != null)
{
Array.Clear(_secretKey, 0, _secretKey.Length);
_secretKey = null;
}
_disposed = true;
}
}
}
// Usage
public class TokenService
{
private readonly IDataProtectionProvider _dataProtectionProvider;
public string GenerateToken(string userId, byte[] secretKey)
{
using var jwtHandler = new SecureJWTHandler(secretKey, _dataProtectionProvider);
try
{
return jwtHandler.CreateToken(userId);
}
finally
{
// Clear secret key
Array.Clear(secretKey, 0, secretKey.Length);
}
}
}
Why this works:
- Multiple security layers: Mutable
byte[]cleared withArray.Clear(), ASP.NET Core Data Protection API for encryption-at-rest, JWT with HMAC-SHA256 + expiration - Controlled memory:
SymmetricSecurityKeywraps byte array for JWT signing; key held only duringSecureJWTHandlerlifetime, cleared on disposal - Defense-in-depth: Data Protection API provides automatic key management/encryption/rotation; keys stored encrypted with machine-specific keys (DPAPI on Windows, keyring on Linux)
- Machine-specific protection: Even if in-memory key compromised, persisted protected version can't be easily decrypted on different machine
- Comprehensive validation:
ClockSkew = TimeSpan.Zeroremoves default tolerance; signature verification and claim extraction while key remains in clearable memory
Secure password hashing with BCrypt
using System;
using BCrypt.Net;
public class SecurePasswordService
{
public string HashPassword(char[] password)
{
// Convert to string for BCrypt (necessary)
string passwordString = new string(password);
try
{
// BCrypt automatically salts and hashes
return BCrypt.Net.BCrypt.HashPassword(passwordString);
}
finally
{
// Clear char array
Array.Clear(password, 0, password.Length);
}
}
public bool VerifyPassword(char[] password, string hash)
{
string passwordString = new string(password);
try
{
// Verify password against hash
return BCrypt.Net.BCrypt.Verify(passwordString, hash);
}
finally
{
// Clear char array
Array.Clear(password, 0, password.Length);
}
}
}
Why this works:
- Purpose-built algorithm: BCrypt designed for password storage with automatic salting, configurable work factor (cost), resistance to rainbow tables and GPU brute-force
- Temporary string trade-off: Converts
char[]to string for BCrypt API, then immediately clearschar[]infinallyto minimize cleartext exposure - Integrated salt management:
HashPassword()generates random salt and stores with hash in single string ($2a$10$...), eliminating separate salt management - Configurable work factor: Default 10-12 rounds (adjustable) makes hashing intentionally slow (~100-300ms), increasing brute-force time while acceptable for auth
- Constant-time verification:
Verify()prevents timing attacks; brief string existence acceptable for well-tested library vs manual PBKDF2/Argon2 implementation
NetworkCredential with SecureString
using System;
using System.Net;
using System.Security;
public class SecureHttpClient
{
public HttpWebRequest CreateAuthenticatedRequest(
string url,
string username,
SecureString password)
{
var request = (HttpWebRequest)WebRequest.Create(url);
// NetworkCredential accepts SecureString
// Handles secure conversion internally
request.Credentials = new NetworkCredential(username, password);
return request;
}
}
// Usage
public void MakeRequest()
{
var password = new SecureString();
// Build SecureString character by character
foreach (char c in GetPasswordFromUser())
{
password.AppendChar(c);
}
// Make password read-only
password.MakeReadOnly();
try
{
var client = new SecureHttpClient();
var request = client.CreateAuthenticatedRequest(
"https://api.example.com",
"username",
password
);
// Use request
}
finally
{
// Dispose SecureString
password.Dispose();
}
}
Why this works:
- Native SecureString support:
NetworkCredential(string userName, SecureString password)constructor handles secure conversion without intermediate cleartext string copies - Character-by-character building:
AppendChar()ensures password never exists as complete immutable string - each char individually encrypted as added - Read-only optimization:
MakeReadOnly()prevents modification and optimizes internal encryption, signaling password is complete - Automatic cleanup:
Dispose()infinallyclears encrypted password even on exceptions;HttpWebRequest/FtpWebRequestmaintain security properties throughout auth - Session reuse: Useful for Windows-authenticated services with password entered once for multiple requests; Windows-specific due to
SecureStringencryption limitations
Verification
After implementing the recommended secure patterns, verify the fix through multiple approaches:
- Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
- Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
- Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
- Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
- Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
- Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
- Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
- Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced
Additional Resources - Note: Windows-only encryption, not recommended for new code
- AesGcm Class - .NET Core 3.0+ authenticated encryption
- SecureString Class
- Array.Clear Method
- SafeHandle Class
- Data Protection API
- CWE-316: Cleartext Storage of Sensitive Information in Memory
- OWASP Secure Coding Practices