Nonce entropy fallback when random_bytes() fails
create_login_nonce() in class-two-factor-core.php (line 1208-1212) has a fallback that produces a weak nonce when random_bytes() fails. On PHP 7+ (the plugin's minimum is 7.2), this fallback is effectively unreachable — random_bytes() uses OS-level CSPRNG sources (getrandom(), arc4random_buf(), CNG-API) that do not fail under normal conditions. This is a code hygiene issue, not an active vulnerability.
try {
$login_nonce['key'] = bin2hex( random_bytes( 32 ) );
} catch ( Exception $ex ) {
$login_nonce['key'] = wp_hash( $user_id . wp_rand() . microtime(), 'nonce' );
}
What the nonce protects
The login nonce is the wp-auth-nonce hidden form field that ties a 2FA submission to the user who was shown the form. It does not protect the verification code itself — the email code uses get_code() (via random_int()), and TOTP codes are generated by the authenticator app.
Why the fallback is weak
If random_bytes() throws, the fallback nonce is built from:
$user_id — known to the attacker (it's in the form as wp-auth-id).
wp_rand() — since WordPress 4.4, this calls random_int(), which uses the same CSPRNG as random_bytes(). If random_bytes() is throwing, random_int() is likely also broken, and wp_rand() falls back to mt_rand(). This is a cascading failure, not an independent weakness.
microtime() — server timestamp in microseconds, narrowable by observing response timing.
These inputs are hashed through wp_hash() (HMAC-SHA256 with the site salt), so exploitation would also require salt compromise.
When would this actually fire?
Only in edge cases that are not realistic on modern hosting:
- Heavily sandboxed container with
/dev/urandom missing and getrandom() blocked.
- Very early boot before entropy pool initialization. (Linux would block, not fail.)
The catch block is defensive code from the PHP 5.x era, when the random_compat polyfill had more failure modes.
Suggestion
Replace the fallback with a hard failure, per Paragon Initiative's recommendation on PR #135: "It would, arguably, be better to fail closed when the CSPRNG is insecure." If the CSPRNG is broken, generating a weak nonce is worse than refusing to proceed.
try {
$login_nonce['key'] = bin2hex( random_bytes( 32 ) );
} catch ( Exception $ex ) {
return false;
}
create_login_nonce() already returns false on other failures (line 1226), and both callers handle it with wp_die().
Nonce entropy fallback when random_bytes() fails
create_login_nonce()inclass-two-factor-core.php(line 1208-1212) has a fallback that produces a weak nonce whenrandom_bytes()fails. On PHP 7+ (the plugin's minimum is 7.2), this fallback is effectively unreachable —random_bytes()uses OS-level CSPRNG sources (getrandom(),arc4random_buf(), CNG-API) that do not fail under normal conditions. This is a code hygiene issue, not an active vulnerability.What the nonce protects
The login nonce is the
wp-auth-noncehidden form field that ties a 2FA submission to the user who was shown the form. It does not protect the verification code itself — the email code usesget_code()(viarandom_int()), and TOTP codes are generated by the authenticator app.Why the fallback is weak
If
random_bytes()throws, the fallback nonce is built from:$user_id— known to the attacker (it's in the form aswp-auth-id).wp_rand()— since WordPress 4.4, this callsrandom_int(), which uses the same CSPRNG asrandom_bytes(). Ifrandom_bytes()is throwing,random_int()is likely also broken, andwp_rand()falls back tomt_rand(). This is a cascading failure, not an independent weakness.microtime()— server timestamp in microseconds, narrowable by observing response timing.These inputs are hashed through
wp_hash()(HMAC-SHA256 with the site salt), so exploitation would also require salt compromise.When would this actually fire?
Only in edge cases that are not realistic on modern hosting:
/dev/urandommissing andgetrandom()blocked.The catch block is defensive code from the PHP 5.x era, when the
random_compatpolyfill had more failure modes.Suggestion
Replace the fallback with a hard failure, per Paragon Initiative's recommendation on PR #135: "It would, arguably, be better to fail closed when the CSPRNG is insecure." If the CSPRNG is broken, generating a weak nonce is worse than refusing to proceed.
create_login_nonce()already returnsfalseon other failures (line 1226), and both callers handle it withwp_die().