Skip to content

CWE-338: Use of Cryptographically Weak PRNG - C# / .NET

Overview

System.Random uses a seeded linear congruential algorithm that is deterministic and not cryptographically secure. An attacker who observes a few outputs from System.Random - or who knows the seed, which defaults to Environment.TickCount - can reconstruct the entire output sequence. This makes it unsuitable for generating security tokens, session identifiers, OTP codes, cryptographic nonces, or any other value that must be unpredictable.

CWE-338 specifically targets the use of a weak PRNG in a security context, while CWE-330 is the broader class of insufficient randomness. In .NET, both manifest as using System.Random where System.Security.Cryptography.RandomNumberGenerator is required. The fix is the same: replace the weak PRNG with the OS-backed CSPRNG.

Primary Defence: Use System.Security.Cryptography.RandomNumberGenerator static methods (GetBytes, GetInt32, GetHexString) for all security-sensitive random values. Keep System.Random only for non-security uses (simulations, shuffling non-sensitive lists, game logic).

Common Vulnerable Patterns

Session Token from System.Random

// VULNERABLE - System.Random seeded from Environment.TickCount (time since boot)
public string GenerateSessionToken()
{
    var random = new Random();
    var bytes = new byte[16];
    random.NextBytes(bytes);
    return Convert.ToHexString(bytes).ToLowerInvariant();
}

Why this is vulnerable:

  • new Random() without an argument uses Environment.TickCount as the seed. An attacker who approximates the server's uptime can enumerate nearby seed values, regenerate the token, and hijack the session.

Predictable CSRF Token

// VULNERABLE - shared Random instance with System.Random
private static readonly Random _rng = new Random();

public string GenerateCsrfToken()
{
    // Two consecutive calls produce correlated values
    return $"{_rng.Next():x8}{_rng.Next():x8}";
}

Why this is vulnerable:

  • System.Random.Next() produces values from a deterministic sequence. Knowing one output allows reconstruction of the seed and prediction of all CSRF tokens, enabling cross-site request forgery.

Password Reset Code from Random.Shared

// VULNERABLE - Random.Shared is thread-safe but still a weak PRNG
public string GeneratePasswordResetCode()
{
    return Random.Shared.Next(100_000, 1_000_000).ToString("D6");
}

Why this is vulnerable:

  • Random.Shared is a convenience API for the same weak System.Random algorithm. Observing a sequence of reset codes allows an attacker to predict codes issued to other users.

Secure Patterns

Cryptographically Secure Token (.NET 6+)

using System.Security.Cryptography;

// SECURE: OS-backed CSPRNG
public static string GenerateToken(int byteLength = 32)
{
    byte[] bytes = RandomNumberGenerator.GetBytes(byteLength);
    // URL-safe Base64, no padding
    return Convert.ToBase64String(bytes)
        .TrimEnd('=')
        .Replace('+', '-')
        .Replace('/', '_');
}

Why this works:

  • RandomNumberGenerator.GetBytes() calls the Windows CNG or Linux /dev/urandom provider. The output is statistically indistinguishable from true random data, making prediction computationally infeasible.

Secure OTP / PIN

using System.Security.Cryptography;

// SECURE: unbiased integer in [fromInclusive, toExclusive)
public static int GenerateOtp() =>
    RandomNumberGenerator.GetInt32(100_000, 1_000_000);

public static string GeneratePin(int digits = 6) =>
    RandomNumberGenerator.GetInt32((int)Math.Pow(10, digits - 1), (int)Math.Pow(10, digits))
        .ToString();

Why this works:

  • RandomNumberGenerator.GetInt32 uses rejection sampling to generate an unbiased result. There is no modulo bias (a subtle weakness in naive rand() % n patterns).

Hex Token and API Key (.NET 5+)

using System.Security.Cryptography;

// SECURE: 64-character hex string (256 bits of entropy)
public static string GenerateApiKey()
{
    byte[] bytes = RandomNumberGenerator.GetBytes(32);
    return Convert.ToHexString(bytes).ToLowerInvariant();
}

// .NET 7+ shorthand
public static string GenerateHexToken(int length = 64)
    => RandomNumberGenerator.GetHexString(length, lowercase: true);

Why this works:

  • Hex encoding is unambiguous and safe in any storage or transport medium without further encoding. At 32 bytes (64 hex chars) the token provides 256 bits of entropy.

Legacy .NET Framework / .NET 5 Fallback

using System.Security.Cryptography;

public static string GenerateTokenLegacy()
{
    using var rng = RandomNumberGenerator.Create();
    byte[] buffer = new byte[32];
    rng.GetBytes(buffer);
    return Convert.ToBase64String(buffer)
        .TrimEnd('=')
        .Replace('+', '-')
        .Replace('/', '_');
}

Why this works:

  • RandomNumberGenerator.Create() returns the platform CSPRNG implementation and is available on all .NET versions. The using statement disposes the instance after use.

Remediation Steps

Locate the Finding

  • Source: Token generation, OTP/PIN creation, session ID generation, nonce creation, key generation
  • Sink: new Random(), Random.Shared.Next(), Random.NextBytes() in security contexts

Apply the Fix

  • PRIORITY 1: Replace new Random() and Random.Shared with RandomNumberGenerator static methods
  • PRIORITY 2: Replace Guid.NewGuid() used as security tokens with RandomNumberGenerator.GetHexString(32)
  • PRIORITY 3: Keep System.Random only for non-security uses; leave a comment explaining the non-security context

Verify the Fix

  • Generate 100 tokens and confirm no two are equal and none follow an obvious pattern
  • Rescan with the security scanner to confirm the finding is resolved

Check for Similar Issues

Search for: new Random(, Random.Shared, Guid.NewGuid() in token/session/key/nonce generation paths

Testing

  • Normal input: exercise each security-sensitive random value flow and confirm tokens, OTPs, nonces, and keys still validate.
  • Boundary input: test short configured lengths, high-volume generation, and concurrent requests for duplicate or malformed values.
  • Malicious input: attempt timestamp-based prediction or replay against newly generated values; confirm all weak PRNG calls were removed.

Additional Resources