Safeguard
Industry Analysis

Secure Random Number Generation in Ruby with SecureRandom

Ruby's built-in rand() uses a predictable Mersenne Twister and should never generate tokens, passwords, or session IDs. Here's how SecureRandom fixes that.

Aman Khan
AppSec Engineer
7 min read

Every Ruby application generates random values constantly: session identifiers, CSRF tokens, password reset links, API keys, one-time passwords. The method you call to produce those values matters more than most developers realize. Ruby ships two very different random number generators under the hood — Kernel#rand (backed by a Mersenne Twister) and the SecureRandom module (backed by the operating system's cryptographic RNG) — and using the wrong one turns "random" into "guessable." This isn't a theoretical concern: security audits of production Rails and Sinatra codebases routinely turn up rand-based token generation in password reset flows, invite links, and API key generators, often inherited from old code or copied from Stack Overflow answers that predate SecureRandom becoming idiomatic. This post breaks down why rand fails for security-sensitive values, what SecureRandom does differently, how much entropy you actually need, and why this specific class of bug matters for software supply chain security.

Why is Ruby's built-in rand unsafe for security-sensitive values?

Kernel#rand and Random.new are unsafe because they're built on the Mersenne Twister (MT19937) algorithm, a PRNG designed for statistical quality and speed, not unpredictability. MT19937 has a period of 2^19937-1 and passes virtually every statistical randomness test, which is exactly why it's the default for simulations, games, and sampling — but it is fully deterministic and, critically, invertible. If an attacker observes 624 consecutive 32-bit outputs from an unseeded Mersenne Twister instance, they can reconstruct its entire internal state and predict every future value it will ever produce, with no brute force required. In a Rails app, that's the equivalent of an attacker who sees a handful of "random" reset tokens or invite codes being able to compute the next ones without touching the server again. This is formally tracked as CWE-338 ("Use of Cryptographically Weak Pseudo-Random Number Generator") and CWE-330 ("Use of Insufficiently Random Values"), both of which show up repeatedly in CVE databases across scripting languages whenever a security token is generated with a general-purpose PRNG instead of a CSPRNG.

What does SecureRandom do differently under the hood?

SecureRandom is safe for tokens because it pulls bytes from the operating system's cryptographically secure random source instead of a seeded, invertible algorithm. It has been part of Ruby's standard library since Ruby 1.8.6, released in March 2007, and on Linux it reads from the kernel's CSPRNG via the getrandom(2) syscall when available, falling back to /dev/urandom on older kernels; on Windows it uses CryptGenRandom/BCryptGenRandom; and when OpenSSL is loaded it can draw from RAND_bytes as well. Because the entropy source is the kernel itself — continuously reseeded from hardware interrupts, timing jitter, and other unpredictable system events — observing any number of past outputs gives an attacker no mathematical way to predict future ones. Ruby 2.5, released in December 2017, specifically improved this by making Random.urandom (which SecureRandom calls internally) prefer getrandom(2) over a raw /dev/urandom read, closing an edge case where processes reading /dev/urandom very early in a system's boot — before the kernel's entropy pool was fully initialized — could receive lower-quality bytes on some older Linux distributions. The module exposes purpose-built methods on top of that byte stream: random_bytes, hex, base64, urlsafe_base64, uuid, random_number, and alphanumeric, so developers rarely need to touch raw bytes directly.

Where do real Ruby and Rails codebases still get this wrong?

Real-world Rails incidents show this isn't a hypothetical mistake — the clearest example is the config/initializers/secret_token.rb era of Rails, where the framework itself generated a signing secret with SecureRandom.hex(64) but developers routinely committed that generated file to public GitHub repositories. When Rails disclosed a YAML deserialization remote code execution vulnerability in January 2013 (CVE-2013-0156), the impact was amplified enormously by the number of apps whose secret_token had leaked publicly: anyone holding that secret could forge signed cookies and trigger RCE without authentication, and security researchers spent weeks scanning GitHub for exposed tokens to responsibly disclose to affected projects. The lesson wasn't that SecureRandom produced a weak secret — it produced a strong one — but that a correctly generated secret is only as good as how it's stored and handled downstream. The more common mistake we still see in 2026-era code review is subtler: developers reach for rand(1_000_000) or Array#sample to build six-digit OTP codes, short invite slugs, or "unique enough" identifiers, not realizing that Kernel#rand's predictability applies even to small integer ranges, and that SecureRandom.random_number(1_000_000) is a drop-in replacement that costs nothing in code complexity.

How much entropy do you actually need, and which method should you call?

The right method depends on the token's purpose, but as a baseline, session identifiers and API keys should carry at least 128 bits of entropy, which SecureRandom.hex(16) provides (16 bytes = 128 bits, rendered as a 32-character hex string). Higher-value secrets like signing keys or long-lived API credentials are commonly generated with SecureRandom.hex(32) for 256 bits. SecureRandom.uuid generates an RFC 4122 version 4 UUID, which looks like 128 bits but actually carries 122 bits of real randomness once the 6 fixed version/variant bits are accounted for — still comfortably strong for most identifier use cases, though not a substitute for a dedicated secret when the value needs to resist targeted guessing rather than just collision. SecureRandom.urlsafe_base64(24) is the standard choice for tokens that get embedded in URLs, such as email confirmation or password reset links, since it avoids characters that require percent-encoding. For numeric OTPs, SecureRandom.random_number(1_000_000) gives you a six-digit code with the same unpredictability guarantees as the byte-level methods, closing the exact gap where rand-based OTP generation tends to sneak back into codebases.

Why does this matter for software supply chain security?

It matters at the supply chain layer because the vulnerable rand call is frequently buried inside a transitive dependency, not your own application code, which means code review of your own repository won't catch it. A gem three or four levels deep in your Gemfile.lock might generate its own internal tokens, cache keys, or nonce values using Kernel#rand — written years ago, before SecureRandom was standard practice, and never revisited because the gem "works fine." Nobody manually audits the RNG choices of every transitive dependency in a modern Ruby application, which commonly pulls in 100+ gems once Rails, its engines, and their own dependencies are counted. This is the same structural problem behind most supply chain security failures: the vulnerability isn't in code you wrote or reviewed, it's in code you inherited by running bundle install, and it changes silently every time a Gemfile.lock gets updated without a corresponding manual review of what changed inside each gem.

How Safeguard Helps

Safeguard is built to catch exactly this category of risk at the dependency layer, where manual review doesn't scale. Rather than relying on developers to remember that Kernel#rand is unsafe for tokens, Safeguard's software composition analysis scans your full Ruby dependency tree — direct and transitive — for insecure randomness patterns, weak cryptographic primitives, and other CWE-338/CWE-330-class findings inside the gems you actually ship, flagging them against known CVEs and OWASP-aligned rules before they reach production. When a Gemfile.lock update pulls in a new or changed transitive dependency, Safeguard's continuous monitoring re-evaluates the resulting SBOM automatically, so a regression introduced by a gem upgrade doesn't sit undetected until an incident forces the question. For teams building the kind of security-sensitive tokens discussed above, Safeguard also supports policy checks that can be wired into CI to block merges introducing known-insecure random number generation or credential-handling patterns, turning "we should really check for this" into an enforced gate rather than a hope. That combination of dependency-level visibility and CI-enforced policy is what lets teams ship Ruby applications with confidence that the randomness underpinning their sessions, tokens, and secrets meets a cryptographic bar, not just a statistical one.

Never miss an update

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