Compromise of 2FA secrets and emergency codes through read-only SQLi – (WPMU Defender <= 3.3.0)

Affected pluginWPMU Defender
Active installs70,000+
Vulnerable version<= 3.3.0
Audited version3.2.0
Fully patched version
Recommended remediationRemoval of the plugin


The plugin stores users’ emergency backup codes and TOTP secrets as plaintext in the database.
An attacker that can obtain one of the seemingly never-ending read-only SQL-Injections will be able to bypass all 2FA checks for all users indefinitely.

Proof of concept

The code for retrieving a user’s TOTP secret:

public static function get_user_secret( $user = null ) {
    // This should only use in testing.
    if ( is_object( $user ) ) {
        $user_id = $user->ID;
    } else {
        $user_id = get_current_user_id();
    $secret = get_user_meta( $user_id, self::TOTP_SECRET_KEY, true );
    if ( ! empty( $secret ) ) {
        return $secret;
    $secret = defender_generate_random_string( self::TOTP_LENGTH, self::TOTP_CHARACTERS );
    update_user_meta( $user_id, self::TOTP_SECRET_KEY, $secret );
    return $secret;

The code for sending an emergency 2FA code per email:

$code = wp_generate_password( 20, false );
update_user_meta( $user->ID, Fallback_Email::FALLBACK_BACKUP_CODE_KEY, [
	'code' => $code,
	'time' => time(),

The highlighted lines above prove that both values are stored as plaintext in the database. Otherwise, it would not be possible to compare them to user input without any conversion.

An attacker can use a read-only SQLi to bypass all 2FA checks for all users indefinitely (in the case of TOTP secrets, emergency tokens have an expiry).

The below query will give an attacker access to all TOTP secrets for all users.

SELECT user_id, meta_value FROM wp_usermeta WHERE meta_key = 'defenderAuthSecret'


Vendor contactedSeptember 07, 2022
First ResponseSeptember 08, 2022
Fully patched at
Publicly disclosedApril 24, 2023


Leave a Reply

Your email address will not be published. Required fields are marked *