Safeguard
Industry Analysis

Secure Random Number Generation in C# with RandomNumberGe...

Why System.Random is a security liability in C# and how RandomNumberGenerator prevents predictable tokens, nonces, and keys in .NET applications.

Aman Khan
AppSec Engineer
6 min read

In September 2023, a security audit of a widely used .NET session-token library found that developers had swapped System.Random in for what should have been a cryptographic generator, producing session IDs that could be predicted after observing just a few hundred samples. It's a mistake that keeps recurring in C# codebases: Random is fast, seedable, and reproducible — exactly the properties you don't want when generating password reset tokens, API keys, or encryption nonces. .NET has shipped a purpose-built alternative, System.Security.Cryptography.RandomNumberGenerator, since the earliest versions of the framework, yet it remains underused relative to how often "randomness" shows up in security-sensitive code. This post explains what makes RandomNumberGenerator different, where teams get it wrong, and how to verify the right primitive is actually in use across a codebase before it ships.

Why is System.Random unsafe for security purposes?

System.Random is unsafe for security purposes because it's a deterministic pseudorandom number generator seeded by a 32-bit integer (by default, derived from Environment.TickCount), which means its entire output sequence can be reconstructed once an attacker knows or narrows down the seed. Microsoft's own documentation states plainly that Random "isn't suitable for use in security-sensitive applications." Because Environment.TickCount only changes roughly every 15-16 milliseconds on Windows and has a limited range of realistic values at any given deployment time, an attacker who knows approximately when a token was generated can brute-force the seed space in seconds. Researchers have repeatedly demonstrated this in practice: given 2-3 consecutive outputs from a Random instance, the internal state can be recovered and all future and past outputs predicted, using publicly documented state-recovery algorithms for the underlying subtractive generator. This is why using Random for password reset tokens, CSRF tokens, or API keys converts a "randomness" bug into a full authentication bypass.

What does RandomNumberGenerator actually do differently?

RandomNumberGenerator differs by sourcing entropy from the operating system's cryptographically secure random number generator instead of a deterministic, seedable algorithm. On Windows it calls into BCryptGenRandom; on Linux it reads from getrandom(2) (or /dev/urandom as a fallback); on macOS it uses arc4random_buf. These OS-level generators are continuously reseeded from hardware entropy sources — interrupt timing, thermal noise, and dedicated hardware RNGs on modern CPUs (Intel's RDRAND, for instance) — so there is no fixed seed for an attacker to recover. Microsoft consolidated the API surface substantially in .NET 6, adding static helper methods like RandomNumberGenerator.GetBytes(int) and RandomNumberGenerator.GetInt32(int, int) that replaced the older, more error-prone pattern of instantiating RNGCryptoServiceProvider, allocating a byte array, and calling GetBytes(buffer) manually. The result is fewer places for a developer to accidentally reuse a buffer, mishandle disposal, or introduce modulo bias when converting bytes to a bounded integer range.

How common is this mistake in real C# codebases?

This mistake shows up often enough that it has its own static analysis rule: CA5394, "Do not use insecure randomness," is a built-in .NET code analyzer warning specifically because the pattern recurs across production codebases. OWASP's Cheat Sheet Series calls out System.Random by name under "Insecure Randomness" in its Cryptographic Storage guidance, and Veracode, Checkmarx, and SonarQube all ship dedicated rules (SonarQube's is S2245) to flag new Random() in files that also handle tokens, passwords, or keys. The pattern is easy to introduce because Random is what appears first in IntelliSense autocomplete and in nearly every beginner C# tutorial on generating "random" values — a Stack Overflow search for "C# generate random string" returns dozens of accepted answers built on System.Random with no mention of the cryptographic alternative. Because the resulting code compiles, runs, and produces output that looks random to a manual reviewer, it routinely passes functional testing and code review and only surfaces during a dedicated security audit or penetration test.

What's the correct way to generate a random token in C#?

The correct approach in modern .NET is to call RandomNumberGenerator.GetBytes(int count) or, for .NET 8 and later, RandomNumberGenerator.GetHexString(int length) and RandomNumberGenerator.GetString(...), rather than hand-rolling byte-to-string conversion. For a typical 256-bit session token, that's RandomNumberGenerator.GetBytes(32) followed by Convert.ToBase64String() or Convert.ToHexString() — both added as convenience methods precisely to close the gap where developers previously wrote their own (often biased) conversion logic. For bounded integers, such as a 6-digit numeric OTP, RandomNumberGenerator.GetInt32(100000, 1000000) is the correct call, because it internally uses rejection sampling to avoid the modulo bias that a naive bytes[0] % 1000000 calculation introduces. On the older RNGCryptoServiceProvider pattern still present in pre-.NET 6 codebases, the class is marked obsolete-by-guidance rather than formally deprecated, so it still compiles and runs without a compiler warning — which is exactly why an automated dependency and pattern scan, not just a dotnet build, is needed to catch it during migration.

Does this matter for cryptographic keys and nonces, not just tokens?

Yes, and arguably more so, because a predictable nonce or IV can break an otherwise sound encryption scheme even when the key itself is strong. AES-GCM, for example, has a well-documented catastrophic failure mode: reusing a nonce with the same key allows an attacker to recover the authentication key and forge subsequent messages, which is why NIST SP 800-38D requires nonces be either generated by a cryptographically secure RNG or managed by a counter that's guaranteed never to repeat. In .NET, both AesGcm and ChaCha20Poly1305 expect callers to supply their own nonce, and the framework does not stop a developer from passing in a System.Random-derived value — the API has no way to distinguish a secure source from an insecure one at the call site. The same logic applies to salt generation for password hashing: Rfc2898DeriveBytes (PBKDF2) and the newer Rfc2898DeriveBytes.Pbkdf2 static method both require a caller-supplied salt, and OWASP's Password Storage Cheat Sheet specifies at least a 16-byte salt drawn from a CSPRNG — a requirement that RandomNumberGenerator.GetBytes(16) satisfies and new Random().NextBytes() does not.

How Safeguard Helps

Catching insecure randomness before it reaches production requires looking at source code and dependency behavior together, not just running dotnet build and hoping the analyzer rules are enabled. Safeguard's software supply chain security platform scans C#/.NET codebases and their dependency trees for exactly this class of issue: instances of System.Random, Guid.NewGuid(), or other non-cryptographic sources used in security-sensitive contexts like token generation, password reset flows, and key derivation, cross-referenced against how that code path is actually reachable in the build. Because CA5394-style findings are easy to suppress or miss when analyzer warnings aren't treated as build-breaking, Safeguard tracks these patterns independently of whatever linting configuration a given repository happens to have enabled, and surfaces them alongside SBOM and provenance data so a security team can see not just that insecure randomness exists, but which shipped artifact it's compiled into. For teams managing SOC 2 or similar compliance obligations, that mapping from source-level weakness to deployed binary is what turns a code review finding into an auditable control — closing the gap between "we have a linter rule" and "we can prove the vulnerable pattern never reached production."

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.