Skip to content

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: SecureString encrypts in memory on Windows (using DPAPI) but only pins memory on Linux/macOS without encryption. For cross-platform applications, consider using byte[] or char[] with explicit clearing instead. Microsoft does not recommend SecureString for 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:

  • SecureString encrypts 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.ZeroFreeBSTR clears memory securely: Zeros unmanaged memory before releasing it
  • finally ensures 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-finally ensures complete cleanup: Both char[] and intermediate byte[] 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 SecureString for .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):

  1. SecureKeyManager - AES-256-GCM encryption with random nonces
  2. SecretEncryption - Session key management & auto-cleanup
  3. ProtectedSecret - High-level API with automatic memory clearing
  4. Your application code using ProtectedSecret

Key management (automatic):

  • SecretEncryption.Instance is 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[]ProtectedPassword encrypts it → stored as encrypted byte[] in RAM
  • When needed: GetPassword() → temporarily decrypts to char[] → use briefly → clear immediately
  • Result: Password only exists in cleartext for microseconds, rest of time encrypted with random session key
SecureKeyManager.cs
    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);
        }
    }
SecretEncryption.cs
    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();
        }
    }
ProtectedSecret.cs
    /// <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;
        }
    }
Example.cs
// 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 in UseSecret() 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[] with finally minimizes 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:

  • SafeHandle ensures 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 if Dispose() 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 with Array.Clear(), ASP.NET Core Data Protection API for encryption-at-rest, JWT with HMAC-SHA256 + expiration
  • Controlled memory: SymmetricSecurityKey wraps byte array for JWT signing; key held only during SecureJWTHandler lifetime, 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.Zero removes 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 clears char[] in finally to 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() in finally clears encrypted password even on exceptions; HttpWebRequest/FtpWebRequest maintain security properties throughout auth
  • Session reuse: Useful for Windows-authenticated services with password entered once for multiple requests; Windows-specific due to SecureString encryption 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