CWE-331: Insufficient Entropy - C#
Overview
Insufficient entropy in C# occurs when using System.Random or Guid.NewGuid() instead of System.Security.Cryptography.RandomNumberGenerator (or RNGCryptoServiceProvider in older .NET) for security-sensitive operations. Random is designed for non-cryptographic purposes and produces predictable sequences.
Primary Defence: Use RandomNumberGenerator.GetBytes() (.NET 6+) or RNGCryptoServiceProvider for all security-sensitive random value generation.
Common Vulnerable Patterns
Using Random with time-based seed for tokens
using System;
// VULNERABLE - Predictable token generation
public class InsecureTokenGenerator
{
// VULNERABLE - Time-based seed
public static string GenerateSessionToken()
{
var random = new Random((int)DateTime.Now.Ticks);
return random.Next().ToString("X");
}
}
Why this is vulnerable:
System.Randomis deterministic and predictable.- Time-based seeds are guessable.
Using Random for encryption keys
using System;
// VULNERABLE - Using Random for encryption key
public static byte[] GenerateKey()
{
var random = new Random();
byte[] key = new byte[32];
random.NextBytes(key); // Predictable!
return key;
}
Why this is vulnerable:
- Keys generated from
System.Randomare predictable. - Observed outputs can reveal future values.
Using Random for API keys
using System;
using System.Text;
// VULNERABLE - API key generation
public static string GenerateApiKey()
{
var random = new Random();
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 32; i++)
{
sb.Append(chars[random.Next(chars.Length)]);
}
return sb.ToString();
}
Why this is vulnerable:
System.Randomis not a CSPRNG.- API keys need high entropy and unpredictability.
Using Guid.NewGuid() for security tokens
using System;
// VULNERABLE - Guid.NewGuid() may use weak RNG
public static string GenerateResetToken()
{
return Guid.NewGuid().ToString(); // Not guaranteed to be cryptographically secure
}
Why this is vulnerable:
- GUIDs are identifiers, not security tokens.
- Entropy and RNG source are not guaranteed for secrets.
Insufficient entropy in PIN generation
using System;
// VULNERABLE - Insufficient entropy (only 4 digits)
public static int GeneratePin()
{
var random = new Random();
return random.Next(10000); // 0000-9999
}
Why this is vulnerable:
- 4 digits is only 10,000 possibilities.
System.Randommakes the sequence guessable.
Secure Patterns
Using RandomNumberGenerator (.NET 6+)
using System;
using System.Security.Cryptography;
using System.Text;
public class SecureTokenGenerator
{
// Generate cryptographically secure token (hex-encoded)
public static string GenerateSecureToken(int bytes = 16)
{
byte[] token = RandomNumberGenerator.GetBytes(bytes);
return Convert.ToHexString(token).ToLower();
}
// Generate base64-encoded token
public static string GenerateBase64Token(int bytes = 32)
{
byte[] token = RandomNumberGenerator.GetBytes(bytes);
return Convert.ToBase64String(token)
.Replace('+', '-')
.Replace('/', '_')
.TrimEnd('='); // URL-safe base64
}
// Generate session ID (256 bits)
public static string GenerateSessionId()
{
return GenerateSecureToken(32); // 64 hex chars
}
// Generate CSRF token (256 bits)
public static string GenerateCsrfToken()
{
return GenerateBase64Token(32);
}
// Generate API key (384 bits)
public static string GenerateApiKey()
{
return GenerateBase64Token(48);
}
// Generate password reset token (256 bits)
public static string GeneratePasswordResetToken()
{
return GenerateSecureToken(32);
}
// Generate numeric PIN with sufficient entropy (min 6 digits)
public static string GenerateSecurePin(int length = 6)
{
if (length < 6)
{
throw new ArgumentException("PIN must be at least 6 digits");
}
StringBuilder pin = new StringBuilder();
byte[] randomBytes = RandomNumberGenerator.GetBytes(length);
for (int i = 0; i < length; i++)
{
pin.Append(randomBytes[i] % 10);
}
return pin.ToString();
}
// Generate cryptographic key for AES-256
public static byte[] GenerateEncryptionKey(int bits = 256)
{
return RandomNumberGenerator.GetBytes(bits / 8);
}
// Generate IV for AES encryption
public static byte[] GenerateIV()
{
return RandomNumberGenerator.GetBytes(16); // 128 bits
}
// Generate random integer in range [0, max)
public static int GetSecureRandomInt(int max)
{
return RandomNumberGenerator.GetInt32(max);
}
}
Why this works:
- Uses OS CSPRNG via
RandomNumberGenerator. - Thread-safe and not seed-predictable.
- 128+ bits of entropy makes guessing infeasible.
Legacy .NET (Framework/Core < 6) with RNGCryptoServiceProvider
using System;
using System.Security.Cryptography;
public class LegacySecureTokenGenerator
{
private static readonly RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
public static string GenerateSecureToken(int bytes = 16)
{
byte[] token = new byte[bytes];
rng.GetBytes(token);
return BitConverter.ToString(token).Replace("-", "").ToLower();
}
public static byte[] GenerateRandomBytes(int length)
{
byte[] bytes = new byte[length];
rng.GetBytes(bytes);
return bytes;
}
}
Why this works:
- Legacy CSPRNG that still uses OS entropy sources.
- Safe for older frameworks when
RandomNumberGeneratoris unavailable.
Complete encryption example with secure randomness
using System;
using System.Security.Cryptography;
using System.Text;
public class SecureEncryption
{
// Generate AES-256 key using secure random
public static byte[] GenerateKey()
{
return RandomNumberGenerator.GetBytes(32); // 256 bits
}
// Encrypt with AES-GCM (authenticated encryption)
// Returns: (nonce, tag, ciphertext)
public static (byte[] nonce, byte[] tag, byte[] ciphertext) Encrypt(
byte[] plaintext,
byte[] key)
{
// Generate secure nonce (96 bits for GCM)
byte[] nonce = RandomNumberGenerator.GetBytes(12);
// Create tag buffer
byte[] tag = new byte[16]; // 128-bit authentication tag
// Create cipher
using var aes = new AesGcm(key);
// Encrypt
byte[] ciphertext = new byte[plaintext.Length];
aes.Encrypt(nonce, plaintext, ciphertext, tag);
return (nonce, tag, ciphertext);
}
// Decrypt AES-GCM ciphertext
public static byte[] Decrypt(
byte[] nonce,
byte[] tag,
byte[] ciphertext,
byte[] key)
{
using var aes = new AesGcm(key);
byte[] plaintext = new byte[ciphertext.Length];
aes.Decrypt(nonce, ciphertext, tag, plaintext);
return plaintext;
}
}
// Usage example
var key = SecureEncryption.GenerateKey();
var plaintext = Encoding.UTF8.GetBytes("sensitive data");
var (nonce, tag, ciphertext) = SecureEncryption.Encrypt(plaintext, key);
var decrypted = SecureEncryption.Decrypt(nonce, tag, ciphertext, key);
Console.WriteLine(Encoding.UTF8.GetString(decrypted)); // "sensitive data"
Why this works:
- Keys and nonces come from a CSPRNG.
- GCM provides confidentiality and integrity.
- Nonce size and randomness avoid reuse risk.
Framework-Specific Guidance
ASP.NET Core - Session Management
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using System.Security.Cryptography;
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict;
options.IdleTimeout = TimeSpan.FromMinutes(30);
// ASP.NET Core uses RandomNumberGenerator for session IDs automatically
});
services.AddAntiforgery(options =>
{
// Anti-forgery tokens use RandomNumberGenerator internally
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});
}
}
Why this works:
- ASP.NET Core uses CSPRNG for session IDs and antiforgery tokens.
- Session/antiforgery config enforces secure defaults.
ASP.NET Core - Custom Token Service
using Microsoft.AspNetCore.DataProtection;
using System.Security.Cryptography;
public interface ITokenService
{
string GenerateVerificationToken();
string GenerateApiKey();
}
public class TokenService : ITokenService
{
private readonly IDataProtectionProvider _dataProtection;
public TokenService(IDataProtectionProvider dataProtection)
{
_dataProtection = dataProtection;
}
public string GenerateVerificationToken()
{
// Generate secure token
byte[] token = RandomNumberGenerator.GetBytes(32);
return Convert.ToBase64String(token);
}
public string GenerateApiKey()
{
// Generate and protect API key
byte[] key = RandomNumberGenerator.GetBytes(48);
string apiKey = Convert.ToBase64String(key);
// Optionally encrypt with Data Protection API
var protector = _dataProtection.CreateProtector("ApiKeys");
return protector.Protect(apiKey);
}
}
Why this works:
- Tokens come from
RandomNumberGenerator, notRandom. - Data Protection can encrypt stored API keys.
Entity Framework Core - API Key Entity
using System.ComponentModel.DataAnnotations;
using System.Security.Cryptography;
public class ApiKey
{
[Key]
public int Id { get; set; }
public string UserId { get; set; }
// Store hashed key, not plaintext!
public string KeyHash { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? ExpiresAt { get; set; }
public static (string plainKey, string hashedKey) GenerateApiKey()
{
// Generate secure random key
byte[] keyBytes = RandomNumberGenerator.GetBytes(48);
string plainKey = Convert.ToBase64String(keyBytes);
// Hash for storage (use SHA256 or better, bcrypt/Argon2)
using var sha256 = SHA256.Create();
byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(plainKey));
string hashedKey = Convert.ToBase64String(hashBytes);
return (plainKey, hashedKey);
}
}
Why this works:
- Keys are generated with CSPRNG entropy.
- Hashing avoids storing plaintext keys.
JWT Security
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
public class JwtTokenProvider
{
// SECURE - Generate signing key with RandomNumberGenerator
private static readonly byte[] _signingKey = RandomNumberGenerator.GetBytes(64);
private static readonly SymmetricSecurityKey _securityKey = new SymmetricSecurityKey(_signingKey);
public static string GenerateJwtToken(string userId)
{
// Generate unique JTI (JWT ID)
string jti = Convert.ToBase64String(RandomNumberGenerator.GetBytes(16));
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, userId),
new Claim(JwtRegisteredClaimNames.Jti, jti)
}),
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(
_securityKey,
SecurityAlgorithms.HmacSha256Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}
Why this works:
- Signing keys and JTI use CSPRNG entropy.
- Short expirations limit token exposure.