Skip to content

Replace weak nonce fallback with hard failure when CSPRNG is unavailable #860

@dknauss

Description

@dknauss

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().

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions