Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : May 9, 2026

CVE-2024-13362: Freemius <= 2.10.1 – Reflected DOM-Based Cross-Site Scripting via url Parameter (wp-fail2ban)

Plugin wp-fail2ban
Severity Medium (CVSS 6.1)
CWE 79
Vulnerable Version 5.3.4
Patched Version 5.4.0
Disclosed April 29, 2026

Analysis Overview

Atomic Edge analysis of CVE-2024-13362:
CVE-2024-13362 is a Reflected DOM-Based Cross-Site Scripting (XSS) vulnerability in the Freemius SDK library, which is used by many WordPress plugins and themes. The vulnerability exists in versions up to 2.10.1. The issue arises from insufficient sanitization of the `url` parameter in the Freemius SDK’s JavaScript, allowing an unauthenticated attacker to inject arbitrary web scripts. The CVSS score is 6.1 (Medium).

The root cause is the lack of output encoding on the `url` parameter within the Freemius JavaScript SDK. The vulnerable code directly uses user-supplied input from the `url` query parameter in a DOM context, such as when constructing a redirect URL or updating `window.location`. The code does not apply any HTML entity encoding or JavaScript escaping before inserting the parameter value into the document. This allows an attacker to break out of the intended context and execute arbitrary JavaScript in the user’s browser.

An attacker can exploit this vulnerability by crafting a malicious link that contains a `url` parameter with a JavaScript payload. For example, a link like `https://example.com/wp-admin/admin.php?page=some-freemius-page&url=javascript:alert(‘XSS’)` would, when clicked by a logged-in administrator, execute the payload in the context of the WordPress admin panel. The attack does not require authentication, but successful exploitation depends on social engineering to trick a user into clicking the link. The injected script executes in the security context of the vulnerable site, allowing the attacker to perform actions on behalf of the victim, such as modifying pages, exfiltrating data, or creating rogue administrator accounts.

The patch (present in the provided diff) does not directly address the Freemius `url` parameter issue because the diff is from a different plugin (WP fail2ban). The patch shown updates the WP fail2ban plugin’s admin code, including formatting changes, removal of a security menu, and addition of badge functionality, but these changes are not related to the CVE. The actual fix for CVE-2024-13362 would require the Freemius SDK to properly escape or validate the `url` parameter before using it in a DOM assignment. Since the provided diff does not contain the vulnerable code, the patch analysis cannot be performed from this code. However, based on the vulnerability description, the fix would involve implementing proper output escaping (e.g., using `encodeURIComponent`, `htmlspecialchars`, or a DOMPurify library) on the `url` parameter value before inserting it into the DOM.

If successfully exploited, an attacker can execute arbitrary JavaScript in the victim’s browser. This can lead to session hijacking, theft of authentication cookies, defacement of the site, forced privilege escalation (if the victim is an admin), and installation of persistent backdoors. The impact is limited by the need for user interaction, but a well-crafted phishing link can still cause significant damage, especially against high-privilege users like site administrators.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/wp-fail2ban/admin/admin.php
+++ b/wp-fail2ban/admin/admin.php
@@ -8,11 +8,13 @@
  */
 namespace orglecklidercharleswordpresswp_fail2ban;

-defined('ABSPATH') or exit;
+use WP_fail2banLibBadgesBadgeManager;

-require_once __DIR__.'/config.php';
-require_once __DIR__.'/widgets.php';
-require_once __DIR__.'/lib/about.php';
+defined( 'ABSPATH' ) or exit;
+
+require_once __DIR__ . '/config.php';
+require_once __DIR__ . '/widgets.php';
+require_once __DIR__ . '/lib/about.php';

 /**
  * Helper: Add a new submenu "under" the Freemius "Add-Ons" submenu
@@ -20,64 +22,63 @@
  * @since  4.4.0        Add type hints
  * @since  4.3.2.1      Backport from 4.3.4.0
  *
- * @param  string       $page_title The text to be displayed in the title tags of the page when the menu
- *                                  is selected.
- * @param  string|null  $capability The capability required for this menu to be displayed to the user.
- * @param  string       $menu_slug  The slug name to refer to this menu by. Should be unique for this menu
- *                                  and only include lowercase alphanumeric, dashes, and underscores characters
- *                                  to be compatible with sanitize_key().
- * @param  callable     $function   The function to be called to output the content for this page.
+ * @param  string      $page_title The text to be displayed in the title tags of the page when the menu
+ *                                 is selected.
+ * @param  string|null $capability The capability required for this menu to be displayed to the user.
+ * @param  string      $menu_slug  The slug name to refer to this menu by. Should be unique for this menu
+ *                                 and only include lowercase alphanumeric, dashes, and underscores characters
+ *                                 to be compatible with sanitize_key().
+ * @param  callable    $function   The function to be called to output the content for this page.
  * @return false|string     The resulting page's hook_suffix, or false if the user does not have the capability required.
  */
-function add_wpf2b_addon_page(string $page_title, ?string $capability, string $menu_slug, $function)
-{
-    global $submenu;
-
-    $menu_title = " - $page_title";
-
-    if (!$capability) {
-        $capability = (is_multisite())
-            ? 'manage_network_options'
-            : 'manage_options';
-    }
-
-    $parent = (wf_fs()->is_activation_mode()) ? null : 'wp-fail2ban-menu';
-    $sub_menu = 'wp-fail2ban-menu-addons';
-
-    if ($hook = add_submenu_page($parent, $page_title, $menu_title, $capability, $menu_slug, $function)) {
-        foreach ($submenu as &$menu) {
-            if (isset($menu[0]) && $menu[0][2] == 'wp-fail2ban-menu') {
-                $new_submenu    = [];
-                $menu_item      = array_pop($menu);
-
-                // Find the submenu we're appending to
-                for ($i = 0; $i < count($menu) && $sub_menu != $menu[$i][2]; $i++) {
-                    $new_submenu[] = $menu[$i];
-                }
-
-                if ($i < count($menu)) {
-                    $new_submenu[] = $menu[$i++];
-                }
-
-                // Find the menu item alphabetically before the new item
-                for (; $i < count($menu) && isset($menu[$i][0]) && 0 === strpos($menu[$i][0], ' - ') && 0 > strcmp($menu[$i][0], $menu_title); $i++) {
-                    $new_submenu[] = $menu[$i];
-                }
-
-                $new_submenu[] = $menu_item;
-
-                for (; $i < count($menu); $i++) {
-                    $new_submenu[] = $menu[$i];
-                }
-
-                $menu = $new_submenu;
-
-                break;
-            }
-        }
-    }
+function add_wpf2b_addon_page( string $page_title, ?string $capability, string $menu_slug, $function ) {
+	global $submenu;
+
+	$menu_title = " - $page_title";
+
+	if ( ! $capability ) {
+		$capability = ( is_multisite() )
+			? 'manage_network_options'
+			: 'manage_options';
+	}
+
+	$parent   = ( wf_fs()->is_activation_mode() ) ? null : 'wp-fail2ban-menu';
+	$sub_menu = 'wp-fail2ban-menu-addons';
+
+	if ( $hook = add_submenu_page( $parent, $page_title, $menu_title, $capability, $menu_slug, $function ) ) {
+		foreach ( $submenu as &$menu ) {
+			if ( isset( $menu[0] ) && $menu[0][2] == 'wp-fail2ban-menu' ) {
+				$new_submenu = array();
+				$menu_item   = array_pop( $menu );
+
+				// Find the submenu we're appending to
+				for ( $i = 0; $i < count( $menu ) && $sub_menu != $menu[ $i ][2]; $i++ ) {
+					$new_submenu[] = $menu[ $i ];
+				}
+
+				if ( $i < count( $menu ) ) {
+					$new_submenu[] = $menu[ $i++ ];
+				}
+
+				// Find the menu item alphabetically before the new item
+				for ( ; $i < count( $menu ) && isset( $menu[ $i ][0] ) && 0 === strpos( $menu[ $i ][0], ' - ' ) && 0 > strcmp( $menu[ $i ][0], $menu_title ); $i++ ) {
+					$new_submenu[] = $menu[ $i ];
+				}
+
+				$new_submenu[] = $menu_item;
+
+				for ( ; $i < count( $menu ); $i++ ) {
+					$new_submenu[] = $menu[ $i ];
+				}
+
+				$menu = $new_submenu;
+
+				break;
+			}
+		}
+	}

-    return $hook;
+	return $hook;
 }

 /**
@@ -86,102 +87,81 @@
  * @since  4.4.0    Add type hints, return type
  * @since  4.3.2.2
  *
- * @param  string   $ver    Version stem, e.g. 4.3.2
- * @param  string   $file   Full path to readme.txt
+ * @param  string $ver    Version stem, e.g. 4.3.2
+ * @param  string $file   Full path to readme.txt
  *
  * @return void
  */
-function readme(string $ver, string $file): void
-{
-    if (file_exists($file)) {
-        $rm = file($file, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES);
-        $inChangelog = $inList = $inSubList = false;
-        foreach ($rm as $line) {
-            if ('== Changelog ==' == $line) {
-                $inChangelog = true;
-                echo "<dl>n";
-                continue;
-            }
-            if ($inChangelog) {
-                if (preg_match('/^= (.*) =$/', $line, $matches) &&
-                    ('Develop' == $matches[1] || substr($matches[1], 0, strlen($ver)) == $ver))
-                {
-                    if ($inList) {
-                        echo "</ul></dd>n";
-                    }
-                    echo "<dt>{$matches[1]}</dt>n";
-                    echo "<dd><ul>n";
-                    $inList = true;
-
-                } elseif (preg_match('/^(s*)* (.*)$/', $line, $matches)) {
-                    $li = $matches[2];
-                    $li = preg_replace('/[(.*?)]((.*?))(.)?/', '<a href="$2" target="_blank" rel="noopener">$1 <span class="dashicons dashicons-external"></span></a>', $li);
-                    $li = preg_replace('/`(.*?)`/', '<tt>$1</tt>', $li);
-                    $li = preg_replace('/**(.*?)**/', '<b>$1</b>', $li);
-                    $li = preg_replace('/*(.*?)*/', '<i>$1</i>', $li);
-                    $li = preg_replace('|((h/t .*?))|', '<span class="ht">$1</span>', $li);
-                    $li = preg_replace('/^(Fix|Deprecate )/', '<span style="color:red">$1</span>', $li);
-                    $li = preg_replace('/^(Update )/', '<span style="color:purple">$1</span>', $li);
-                    $li = preg_replace('/^(Site Health|WAF: )/', '<span style="color:blue">$1</span>', $li);
-                    $li = str_replace('[Premium only]', '<span style="color: grey; font-style: italic; font-weight: bold">Premium only</span>', $li);
-                    if (!$inSubList && strlen($matches[1])) {
-                        echo '<ul>';
-                        $inSubList = true;
-                    } elseif ($inSubList && !strlen($matches[1])) {
-                        echo '</ul>';
-                        $inSubList = false;
-                    }
-                    echo "<li>{$li}</li>n";
-
-                } else {
-                    if ($inList) {
-                        echo "</ul></dd>n";
-                        $inList = false;
-                    }
-                    echo "</dl>n";
-                    $inChangelog = false;
-
-                    break;
-                }
-            }
-        }
-    } else {
-        echo "<p>Error: Cannot open <tt>readme.txt</tt></p>n";
-    }
+function readme( string $ver, string $file ): void {
+	if ( file_exists( $file ) ) {
+		$rm          = file( $file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
+		$inChangelog = $inList = $inSubList = false;
+		foreach ( $rm as $line ) {
+			if ( '== Changelog ==' == $line ) {
+				$inChangelog = true;
+				echo "<dl>n";
+				continue;
+			}
+			if ( $inChangelog ) {
+				if ( preg_match( '/^= (.*) =$/', $line, $matches ) &&
+					( 'Develop' == $matches[1] || substr( $matches[1], 0, strlen( $ver ) ) == $ver ) ) {
+					if ( $inList ) {
+						echo "</ul></dd>n";
+					}
+					echo "<dt>{$matches[1]}</dt>n";
+					echo "<dd><ul>n";
+					$inList = true;
+
+				} elseif ( preg_match( '/^(s*)* (.*)$/', $line, $matches ) ) {
+					$li = $matches[2];
+					$li = preg_replace( '/[(.*?)]((.*?))(.)?/', '<a href="$2" target="_blank" rel="noopener">$1 <span class="dashicons dashicons-external"></span></a>', $li );
+					$li = preg_replace( '/`(.*?)`/', '<tt>$1</tt>', $li );
+					$li = preg_replace( '/**(.*?)**/', '<b>$1</b>', $li );
+					$li = preg_replace( '/*(.*?)*/', '<i>$1</i>', $li );
+					$li = preg_replace( '|((h/t .*?))|', '<span class="ht">$1</span>', $li );
+					$li = preg_replace( '/^(Fix|Deprecate )/', '<span style="color:red">$1</span>', $li );
+					$li = preg_replace( '/^(Update )/', '<span style="color:purple">$1</span>', $li );
+					$li = preg_replace( '/^(Site Health|WAF: )/', '<span style="color:blue">$1</span>', $li );
+					$li = preg_replace( '/[((Canonical|Premium) only)]/', '<span style="color: grey; font-style: italic; font-weight: bold">$1</span>', $li );
+					if ( ! $inSubList && strlen( $matches[1] ) ) {
+						echo '<ul>';
+						$inSubList = true;
+					} elseif ( $inSubList && ! strlen( $matches[1] ) ) {
+						echo '</ul>';
+						$inSubList = false;
+					}
+					echo "<li>{$li}</li>n";
+
+				} else {
+					if ( $inList ) {
+						echo "</ul></dd>n";
+						$inList = false;
+					}
+					echo "</dl>n";
+					$inChangelog = false;
+
+					break;
+				}
+			}
+		}
+	} else {
+		echo "<p>Error: Cannot open <tt>readme.txt</tt></p>n";
+	}
 }

 /**
  * Helper: Security and Settings menu
  *
+ * @since  5.4.0    Drop security page
  * @since  4.4.0    Add return type
  * @since  4.3.0
  *
- * @param  string   $capability Capability
+ * @param  string $capability Capability
  *
  * @return void
  */
-function _security_settings(string $capability = 'manage_options'): void
-{
-    if (function_exists('add_security_page')) {
-        if ($hook = add_security_page(
-            'WP fail2ban',
-            'WP fail2ban',
-            plugin_basename(WP_FAIL2BAN_DIR),
-            __NAMESPACE__.'security'
-        )) {
-            add_action("load-$hook", function () {
-                wp_enqueue_style('wpf2b-admin', plugins_url('css/admin.css', __FILE__));
-                apply_filters('wp_fail2ban_init_tabs', false);
-                TabBase::setDefaultTab('logging');
-                TabBase::getActiveTab()->current_screen();
-            });
-            if (class_exists(__NAMESPACE__.'premiumWPf2b')) {
-                _settings('about', $capability);
-            }
-        }
-    } else {
-        _settings(apply_filters(__METHOD__.'.page', 'logging'), $capability);
-    }
+function _security_settings( string $capability = 'manage_options' ): void {
+	_settings( apply_filters( __METHOD__ . '.page', 'logging' ), $capability );
 }

 /**
@@ -195,23 +175,25 @@
  *
  * @return void
  */
-function _settings(string $page = null, string $capability = 'manage_options'): void
-{
-    if ($hook = add_submenu_page(
-        'wp-fail2ban-menu',
-        __('Settings', 'wp-fail2ban'),
-        __('Settings', 'wp-fail2ban'),
-        $capability,
-        'wpf2b-settings',
-        __NAMESPACE__.'settings'
-    )) {
-        add_action("load-$hook", function () use ($page) {
-            wp_enqueue_style('wpf2b-admin', plugins_url('css/admin.css', __FILE__));
-            apply_filters('wp_fail2ban_init_tabs', false);
-            TabBase::setDefaultTab($page);
-            TabBase::getActiveTab()->current_screen();
-        });
-    }
+function _settings( string $page = null, string $capability = 'manage_options' ): void {
+	if ( $hook = add_submenu_page(
+		'wp-fail2ban-menu',
+		__( 'Settings', 'wp-fail2ban' ),
+		__( 'Settings', 'wp-fail2ban' ),
+		$capability,
+		'wpf2b-settings',
+		__NAMESPACE__ . 'settings'
+	) ) {
+		add_action(
+			"load-$hook",
+			function () use ( $page ) {
+				wp_enqueue_style( 'wpf2b-admin', plugins_url( 'css/admin.css', __FILE__ ) );
+				apply_filters( 'wp_fail2ban_init_tabs', false );
+				TabBase::setDefaultTab( $page );
+				TabBase::getActiveTab()->current_screen();
+			}
+		);
+	}
 }

 /**
@@ -222,29 +204,31 @@
  *
  * @return void
  */
-function admin_menu(): void
-{
-    if (!is_multisite()) {
-        if ((defined('WP_FAIL2BAN_FREE_ONLY') && false !== WP_FAIL2BAN_FREE_ONLY) || !wf_fs()->can_use_premium_code()) {
-            $hook = add_menu_page(
-                'WP fail2ban',
-                'WP fail2ban',
-                'manage_options',
-                'wp-fail2ban-menu',
-                __NAMESPACE__.'welcome',
-                plugin_dir_url(WP_FAIL2BAN_FILE).'assets/menu.svg'
-            );
-            add_action("load-$hook", function () {
-                wp_enqueue_style('wpf2b-admin', plugins_url('css/admin.css', __FILE__));
-            });
-            add_action('admin_menu', __NAMESPACE__.'admin_menu_fix', PHP_INT_MAX);
-            do_action('wp_fail2ban_menu');
-
-            _security_settings();
-        }
-    }
+function admin_menu(): void {
+	if ( ! is_multisite() ) {
+		if ( ( defined( 'WP_FAIL2BAN_FREE_ONLY' ) && false !== WP_FAIL2BAN_FREE_ONLY ) || ! wf_fs()->can_use_premium_code() ) {
+			$hook = add_menu_page(
+				'WP fail2ban',
+				'WP fail2ban',
+				'manage_options',
+				'wp-fail2ban-menu',
+				__NAMESPACE__ . 'welcome',
+				plugin_dir_url( WP_FAIL2BAN_FILE ) . 'assets/menu.svg'
+			);
+			add_action(
+				"load-$hook",
+				function () {
+					wp_enqueue_style( 'wpf2b-admin', plugins_url( 'css/admin.css', __FILE__ ) );
+				}
+			);
+			add_action( 'admin_menu', __NAMESPACE__ . 'admin_menu_fix', PHP_INT_MAX );
+			do_action( 'wp_fail2ban_menu' );
+
+			_security_settings();
+		}
+	}
 }
-add_action('admin_menu', __NAMESPACE__.'admin_menu');
+add_action( 'admin_menu', __NAMESPACE__ . 'admin_menu' );

 /**
  * Register network admin menus
@@ -254,13 +238,12 @@
  *
  * @return void
  */
-function network_admin_menu(): void
-{
-    if ((defined('WP_FAIL2BAN_FREE_ONLY') && false !== WP_FAIL2BAN_FREE_ONLY) || !wf_fs()->can_use_premium_code()) {
-        _network_admin_menu();
-    }
+function network_admin_menu(): void {
+	if ( ( defined( 'WP_FAIL2BAN_FREE_ONLY' ) && false !== WP_FAIL2BAN_FREE_ONLY ) || ! wf_fs()->can_use_premium_code() ) {
+		_network_admin_menu();
+	}
 }
-add_action('network_admin_menu', __NAMESPACE__.'network_admin_menu');
+add_action( 'network_admin_menu', __NAMESPACE__ . 'network_admin_menu' );

 /**
  * Actual network admin menu handler
@@ -270,44 +253,46 @@
  *
  * @return void
  */
-function _network_admin_menu(): void
-{
-    $hook = add_menu_page(
-        'WP fail2ban',
-        'WP fail2ban',
-        'manage_options',
-        'wp-fail2ban-menu',
-        __NAMESPACE__.'welcome',
-        plugin_dir_url(WP_FAIL2BAN_FILE).'assets/menu.svg'
-    );
-    add_action("load-$hook", function () {
-        wp_enqueue_style('wpf2b-admin', plugins_url('css/admin.css', __FILE__));
-    });
-    add_action('network_admin_menu', __NAMESPACE__.'admin_menu_fix', PHP_INT_MAX);
-    do_action('wp_fail2ban_menu');
+function _network_admin_menu(): void {
+	$hook = add_menu_page(
+		'WP fail2ban',
+		'WP fail2ban',
+		'manage_options',
+		'wp-fail2ban-menu',
+		__NAMESPACE__ . 'welcome',
+		plugin_dir_url( WP_FAIL2BAN_FILE ) . 'assets/menu.svg'
+	);
+	add_action(
+		"load-$hook",
+		function () {
+			wp_enqueue_style( 'wpf2b-admin', plugins_url( 'css/admin.css', __FILE__ ) );
+		}
+	);
+	add_action( 'network_admin_menu', __NAMESPACE__ . 'admin_menu_fix', PHP_INT_MAX );
+	do_action( 'wp_fail2ban_menu' );

-    _security_settings();
+	_security_settings();
 }

 /**
  * Fix first submenu name.
  *
+ * @since  5.4.0    Handle incomplete submenu
  * @since  4.4.0    Add return type
  * @since  4.3.0
  *
  * @return void
  */
-function admin_menu_fix(): void
-{
-    global $submenu;
-
-    if (isset($submenu['wp-fail2ban-menu']) && 'WP fail2ban' == @$submenu['wp-fail2ban-menu'][0][0]) {
-        $submenu['wp-fail2ban-menu'][0][0] = __('Welcome', 'wp-fail2ban');
-    }
-
-    if (defined('WP_FAIL2BAN_FREE_ONLY') && WP_FAIL2BAN_FREE_ONLY) {
-        remove_submenu_page('wp-fail2ban-menu', 'wp-fail2ban-menu-pricing');
-    }
+function admin_menu_fix(): void {
+	global $submenu;
+
+	if ( 'WP fail2ban' == ( $submenu['wp-fail2ban-menu'][0][0] ?? '' ) ) {
+		$submenu['wp-fail2ban-menu'][0][0] = __( 'Welcome', 'wp-fail2ban' );
+	}
+
+	if ( defined( 'WP_FAIL2BAN_FREE_ONLY' ) && WP_FAIL2BAN_FREE_ONLY ) {
+		remove_submenu_page( 'wp-fail2ban-menu', 'wp-fail2ban-menu-pricing' );
+	}
 }

 /**
@@ -318,76 +303,69 @@
  * @since  4.4.0    Add type hints, return type
  * @since  4.2.0
  *
- * @param  string[]     $actions        An array of plugin action links. By default this can include 'activate',
- *                                      'deactivate', and 'delete'.
- * @param  string       $plugin_file    Path to the plugin file relative to the plugins directory.
- * @param  array|null   $plugin_data    An array of plugin data. See `get_plugin_data()`.
- * @param  string       $context        The plugin context. By default this can include 'all', 'active', 'inactive',
- *                                      'recently_activated', 'upgrade', 'mustuse', 'dropins', and 'search'.
+ * @param  string[]   $actions        An array of plugin action links. By default this can include 'activate',
+ *                                    'deactivate', and 'delete'.
+ * @param  string     $plugin_file    Path to the plugin file relative to the plugins directory.
+ * @param  array|null $plugin_data    An array of plugin data. See `get_plugin_data()`.
+ * @param  string     $context        The plugin context. By default this can include 'all', 'active', 'inactive',
+ *                                    'recently_activated', 'upgrade', 'mustuse', 'dropins', and 'search'.
  *
  * @return string[]
  *
  * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  */
-function plugin_action_links(array $actions, string $plugin_file, ?array $plugin_data, string $context): array
-{
-    if (preg_match("|$plugin_file$|", WP_FAIL2BAN_FILE) &&
-        (!is_multisite() || is_network_admin()))
-    {
-        foreach (get_mu_plugins() as $plugin => $data) {
-            if (0 === strpos($data['Name'], 'WP fail2ban')) {
-                // MU plugin
-                //
-                // * Make sure the "normal" plugin is activated
-                // * Remove "Deactivate" and "Delete" links
-                $plugin = plugin_basename(WP_FAIL2BAN_FILE);
-
-                if (array_key_exists($plugin, get_plugins()) && !is_plugin_active($plugin)) {
-                    activate_plugin(
-                        $plugin,
-                        '',     // don't redirect anywhere
-                        false,
-                        true    // don't call activation hooks
-                    );
-                }
-
-                unset($actions['deactivate']);
-                unset($actions['delete']);
-                break;
-            }
-        }
-
-        // No settings tabs for ClassicPress + Free
-        if (function_exists('add_security_page') &&
-            !wf_fs()->can_use_premium_code())
-        {
-            return $actions;
-        }
-
-        if (!wf_fs()->is_activation_mode() &&
-            (!wf_fs()->can_use_premium_code() ||
-            ((is_multisite() && wf_fs()->is_plan_or_trial('silver')) ||
-            (!is_multisite() && wf_fs()->is_plan_or_trial('bronze')))))
-        {
-            $settings = sprintf(
-                '<a href="%s?page=wpf2b-settings&tab=about" title="%s">%s</a>',
-                network_admin_url('admin.php'),
-                __('Settings', 'wp-fail2ban'),
-                (function_exists('add_security_page'))
-                    ? '<span class="dashicon dashicons-admin-generic"></span>'
-                    : __('Settings', 'wp-fail2ban')
-            );
-            // Add Settings at the start
-            $actions = array_merge([
-                'settings' => $settings
-            ], $actions);
-        }
-    }
+function plugin_action_links( array $actions, string $plugin_file, ?array $plugin_data, string $context ): array {
+	if ( preg_match( "|$plugin_file$|", WP_FAIL2BAN_FILE ) &&
+		( ! is_multisite() || is_network_admin() ) ) {
+		foreach ( get_mu_plugins() as $plugin => $data ) {
+			if ( 0 === strpos( $data['Name'], 'WP fail2ban' ) ) {
+				// MU plugin
+				//
+				// * Make sure the "normal" plugin is activated
+				// * Remove "Deactivate" and "Delete" links
+				$plugin = plugin_basename( WP_FAIL2BAN_FILE );
+
+				if ( array_key_exists( $plugin, get_plugins() ) && ! is_plugin_active( $plugin ) ) {
+					activate_plugin(
+						$plugin,
+						'',     // don't redirect anywhere
+						false,
+						true    // don't call activation hooks
+					);
+				}
+
+				unset( $actions['deactivate'] );
+				unset( $actions['delete'] );
+				break;
+			}
+		}
+
+		if ( ! wf_fs()->is_activation_mode() &&
+			( ! wf_fs()->can_use_premium_code() ||
+			( ( is_multisite() && wf_fs()->is_plan_or_trial( 'silver' ) ) ||
+			( ! is_multisite() && wf_fs()->is_plan_or_trial( 'bronze' ) ) ) ) ) {
+			$settings = sprintf(
+				'<a href="%s?page=wpf2b-settings&tab=about" title="%s">%s</a>',
+				network_admin_url( 'admin.php' ),
+				__( 'Settings', 'wp-fail2ban' ),
+				( function_exists( 'add_security_page' ) )
+					? '<span class="dashicon dashicons-admin-generic"></span>'
+					: __( 'Settings', 'wp-fail2ban' )
+			);
+			// Add Settings at the start
+			$actions = array_merge(
+				array(
+					'settings' => $settings,
+				),
+				$actions
+			);
+		}
+	}

-    return $actions;
+	return $actions;
 }
-add_filter('plugin_action_links', __NAMESPACE__.'plugin_action_links', PHP_INT_MAX, 4);
-add_filter('network_admin_plugin_action_links', __NAMESPACE__.'plugin_action_links', 99999, 4);
+add_filter( 'plugin_action_links', __NAMESPACE__ . 'plugin_action_links', PHP_INT_MAX, 4 );
+add_filter( 'network_admin_plugin_action_links', __NAMESPACE__ . 'plugin_action_links', 99999, 4 );

 /**
  * Add help tab to Dashboard
@@ -397,35 +375,57 @@
  *
  * @return void
  */
-function admin_head_dashboard(): void
-{
-    $content = '';
-
-    if ((!is_multisite() && current_user_can('manage_options')) ||
-        (is_network_admin() && current_user_can('manage_network_options')))
-    {
-        $message = __('Shows the last 5 messages sent to <code>syslog</code> - provides simple status at a glance and can be helpful for debugging.', 'wp-fail2ban');
-        if (!wf_fs()->can_use_premium_code()) {
-            $message .= sprintf(
-                /* translators: %s: <a href> internals */
-                __('The <a %s>Premium version</a> provides more advanced views.', 'wp-fail2ban'),
-                sprintf('href="%s"', network_admin_url('admin.php?page=wp-fail2ban-menu-pricing'))
-            );
-        }
-
-        $content = sprintf(
-            '<p><strong>%s</strong> — %s.</p>',
-            __('Last 5 Messages', 'wp-fail2ban'),
-            $message
-        );
-    }
-    if (!empty($content = apply_filters('wp_fail2ban_dashboard_help', $content))) {
-        get_current_screen()->add_help_tab([
-            'id'        => 'wp-fail2ban',
-            'title'     => 'WP fail2ban',
-            'content'   => $content
-        ]);
-    }
+function admin_head_dashboard(): void {
+	$content = '';
+
+	if ( ( ! is_multisite() && current_user_can( 'manage_options' ) ) ||
+		( is_network_admin() && current_user_can( 'manage_network_options' ) ) ) {
+		$message = __( 'Shows the last 5 messages sent to <code>syslog</code> - provides simple status at a glance and can be helpful for debugging.', 'wp-fail2ban' );
+		if ( ! wf_fs()->can_use_premium_code() ) {
+			$message .= sprintf(
+				/* translators: %s: <a href> internals */
+				__( 'The <a %s>Premium version</a> provides more advanced views.', 'wp-fail2ban' ),
+				sprintf( 'href="%s"', network_admin_url( 'admin.php?page=wp-fail2ban-menu-pricing' ) )
+			);
+		}
+
+		$content = sprintf(
+			'<p><strong>%s</strong> — %s.</p>',
+			__( 'Last 5 Messages', 'wp-fail2ban' ),
+			$message
+		);
+	}
+	if ( ! empty( $content = apply_filters( 'wp_fail2ban_dashboard_help', $content ) ) ) {
+		get_current_screen()->add_help_tab(
+			array(
+				'id'      => 'wp-fail2ban',
+				'title'   => 'WP fail2ban',
+				'content' => $content,
+			)
+		);
+	}
 }
-add_action('admin_head-index.php', __NAMESPACE__.'admin_head_dashboard', 9999);
+add_action( 'admin_head-index.php', __NAMESPACE__ . 'admin_head_dashboard', 9999 );
+
+/**
+ * Add badges to plugin listing
+ *
+ * @since  5.4.0
+ *
+ * @return void
+ */
+function admin_init(): void {
+	require_once WP_FAIL2BAN_DIR . '/vendor/wp-fail2ban/lib-badges/src/BadgeManager.php';

+	BadgeManager::init(
+		array(
+			'plugin_file' => WP_FAIL2BAN_FILE,
+			'free'        => true, // @wpf2b-free-only
+			'lts'         => true,
+			'show'        => array(
+				'non-canonical' => false,
+			),
+		)
+	);
+}
+add_action( 'admin_init', __NAMESPACE__ . 'admin_init' );
--- a/wp-fail2ban/admin/config.php
+++ b/wp-fail2ban/admin/config.php
@@ -8,7 +8,7 @@
  */
 namespace orglecklidercharleswordpresswp_fail2ban;

-defined('ABSPATH') or exit;
+defined( 'ABSPATH' ) or exit;

 require_once 'lib/tab.php';
 require_once 'lib/logging.php';
@@ -20,20 +20,18 @@

 /**
  * Init
- *
  */
-function init_tabs($init)
-{
-    if (!$init) {
-        new TabBlock();
-        new TabLogging();
-        new TabPlugins();
-        new TabRemoteIPs();
-        new TabSyslog();
-    }
-    return true;
+function init_tabs( $init ) {
+	if ( ! $init ) {
+		new TabBlock();
+		new TabLogging();
+		new TabPlugins();
+		new TabRemoteIPs();
+		new TabSyslog();
+	}
+	return true;
 }
-add_filter('wp_fail2ban_init_tabs', __NAMESPACE__.'init_tabs');
+add_filter( 'wp_fail2ban_init_tabs', __NAMESPACE__ . 'init_tabs' );

 /**
  * Get network settings messages.
@@ -45,31 +43,30 @@
  *
  * @return void
  */
-function admin_notices(): void
-{
-    global $wp_settings_errors;
-
-    $screen = get_current_screen();
-    switch ($screen->id) {
-        default:
-            if (false === apply_filters(__FUNCTION__, false, $screen->id)) {
-                break;
-            }
-        case 'security_page_wp-fail2ban-premium-network':
-        case 'wp-fail2ban_page_wpf2b-settings-network':
-            if ($transients = get_site_transient('settings_errors')) {
-                $wp_settings_errors = array_merge((array)$wp_settings_errors, $transients);
-                delete_site_transient('settings_errors');
-            }
-            // fallthrough
-        case 'security_page_wp-fail2ban-premium':
-        case 'wp-fail2ban_page_wpf2b-settings':
-            settings_errors();
-            break;
-    }
+function admin_notices(): void {
+	global $wp_settings_errors;
+
+	$screen = get_current_screen();
+	switch ( $screen->id ) {
+		default:
+			if ( false === apply_filters( __FUNCTION__, false, $screen->id ) ) {
+				break;
+			}
+		case 'security_page_wp-fail2ban-premium-network':
+		case 'wp-fail2ban_page_wpf2b-settings-network':
+			if ( $transients = get_site_transient( 'settings_errors' ) ) {
+				$wp_settings_errors = array_merge( (array) $wp_settings_errors, $transients );
+				delete_site_transient( 'settings_errors' );
+			}
+			// fallthrough
+		case 'security_page_wp-fail2ban-premium':
+		case 'wp-fail2ban_page_wpf2b-settings':
+			settings_errors();
+			break;
+	}
 }
-add_action('admin_notices', __NAMESPACE__.'admin_notices');
-add_action('network_admin_notices', __NAMESPACE__.'admin_notices');
+add_action( 'admin_notices', __NAMESPACE__ . 'admin_notices' );
+add_action( 'network_admin_notices', __NAMESPACE__ . 'admin_notices' );

 /**
  * Render Security settings.
@@ -79,19 +76,18 @@
  *
  * @return void
  */
-function security(): void
-{
-    $tabs = [
-        'logging',
-        'syslog',
-        'block',
-        'remote-ips',
-        'plugins'
-    ];
-    $tabs = apply_filters(__METHOD__.'.tabs', $tabs);
-    $page = apply_filters(__METHOD__.'.page', plugin_basename(WP_FAIL2BAN_DIR));
+function security(): void {
+	$tabs = array(
+		'logging',
+		'syslog',
+		'block',
+		'remote-ips',
+		'plugins',
+	);
+	$tabs = apply_filters( __METHOD__ . '.tabs', $tabs );
+	$page = apply_filters( __METHOD__ . '.page', plugin_basename( WP_FAIL2BAN_DIR ) );

-    render_tabs($tabs, $page, __('Security', 'wp-fail2ban'));
+	render_tabs( $tabs, $page, __( 'Security', 'wp-fail2ban' ) );
 }

 /**
@@ -103,22 +99,21 @@
  *
  * @return void
  */
-function settings(): void
-{
-    $tabs = [];
-
-    if (!function_exists('add_security_page')) {
-        $tabs = [
-            'logging',
-            'syslog',
-            'block',
-            'remote-ips',
-            'plugins'
-        ];
-    }
-    $tabs = apply_filters(__METHOD__.'.tabs', $tabs);
+function settings(): void {
+	$tabs = array();
+
+	if ( ! function_exists( 'add_security_page' ) ) {
+		$tabs = array(
+			'logging',
+			'syslog',
+			'block',
+			'remote-ips',
+			'plugins',
+		);
+	}
+	$tabs = apply_filters( __METHOD__ . '.tabs', $tabs );

-    render_tabs($tabs, 'wpf2b-settings');
+	render_tabs( $tabs, 'wpf2b-settings' );
 }

 /**
@@ -127,58 +122,62 @@
  * @since  4.4.0    Add type hints, return type
  * @since  4.3.0
  *
- * @param  array    $tabs       List of slugs of tabs to render
- * @param  string   $menu       Menu slug
+ * @param  array  $tabs       List of slugs of tabs to render
+ * @param  string $menu       Menu slug
  *
  * @return void
  */
-function render_tabs(array $tabs, string $menu, string $title = null): void
-{
-    if (is_null($title)) {
-        $title = __('Settings', 'wp-fail2ban');
-    }
-    $active_tab = TabBase::getActiveTab();
+function render_tabs( array $tabs, string $menu, string $title = null ): void {
+	if ( is_null( $title ) ) {
+		$title = __( 'Settings', 'wp-fail2ban' );
+	}
+	$active_tab = TabBase::getActiveTab();

-    ?>
+	?>
 <div class="wrap" id="wp-fail2ban" style="margin-top: 0">
-    <?=apply_filters(__METHOD__.'.title', sprintf('<h1>%s</h1>', $title))?>
-  <hr class="wp-header-end">
+	<?php echo apply_filters( __METHOD__ . '.title', sprintf( '<h1>%s</h1>', $title ) ); ?>
+	<hr class="wp-header-end">

-  <h2 class="nav-tab-wrapper wp-clearfix">
-    <?php
-    foreach ($tabs as $slug) {
-        $class = 'nav-tab';
-        if ($active_tab->getSlug() == $slug) {
-            $class .= ' nav-tab-active';
-        }
-        $params = apply_filters(__METHOD__.'.params', [
-            'page' => $menu,
-            'tab' => $slug
-        ]);
-        printf('<a class="%s" href="?%s">%s</a>', $class, http_build_query($params), TabBase::getTabName($slug));
-    }
-    ?>
-  </h2>
-
-    <?php
-    // Because the settings API was never finished we need an ugly hack
-    $action = sprintf(
-        '%s?tab=%s',
-        admin_url(is_network_admin()
-            ? 'admin-post.php'
-            : 'options.php'),
-        $active_tab->getSlug()
-    );
-    ?>
-
-  <form action="<?=$action?>" method="post">
-    <?php
-    settings_fields('wp-fail2ban');
-    $active_tab->render();
-    ?>
-  </form>
+	<h2 class="nav-tab-wrapper wp-clearfix">
+	<?php
+	foreach ( $tabs as $slug ) {
+		$class = 'nav-tab';
+		if ( $active_tab->getSlug() == $slug ) {
+			$class .= ' nav-tab-active';
+		}
+		$params = apply_filters(
+			__METHOD__ . '.params',
+			array(
+				'page' => $menu,
+				'tab'  => $slug,
+			)
+		);
+		printf( '<a class="%s" href="?%s">%s</a>', $class, http_build_query( $params ), TabBase::getTabName( $slug ) );
+	}
+	?>
+	</h2>
+
+	<?php
+	// Because the settings API was never finished we need an ugly hack
+	$action = sprintf(
+		'%s?tab=%s',
+		admin_url(
+			is_network_admin()
+			? 'admin-post.php'
+			: 'options.php'
+		),
+		$active_tab->getSlug()
+	);
+	?>
+
+	<form action="<?php echo $action; ?>" method="post">
+	<?php
+	settings_fields( 'wp-fail2ban' );
+	$active_tab->render();
+	?>
+	</form>
 </div>
-    <?php
+	<?php
 }

 /**
@@ -187,11 +186,10 @@
  * @since  4.4.0    Add type hint
  * @since  4.3.0
  *
- * @param  string   $define
+ * @param  string $define
  * @return mixed
  */
-function have_defined(string $define)
-{
-    return apply_filters(__NAMESPACE__.'have_defined', defined($define), $define);
+function have_defined( string $define ) {
+	return apply_filters( __NAMESPACE__ . 'have_defined', defined( $define ), $define );
 }

--- a/wp-fail2ban/admin/config/block.php
+++ b/wp-fail2ban/admin/config/block.php
@@ -6,173 +6,177 @@
  * @since   4.4.0   Require PHP 7.4
  * @since   4.0.0
  */
-namespace    orglecklidercharleswordpresswp_fail2ban;
+namespace orglecklidercharleswordpresswp_fail2ban;

-defined('ABSPATH') or exit;
+defined( 'ABSPATH' ) or exit;

 /**
  * Tab: Block
  *
  * @since 4.0.0
  */
-class TabBlock extends TabBase
-{
-    /**
-     * Settings page slug
-     *
-     * @since 4.3.2.1
-     */
-    const SETTINGS_PAGE = 'wp-fail2ban-block';
-
-    /**
-     * Override Docs link
-     *
-     * @since 4.3.2.1
-     */
-    const HELP_LINK_DOCS = 'https://life-with.wp-fail2ban.com/core/configuration/settings/block/';
-    /**
-     * Override Reference link
-     *
-     * @since 4.3.2.1
-     */
-    const HELP_LINK_REFERENCE = 'https://docs.wp-fail2ban.com/en/'.WP_FAIL2BAN_VER2.'/defines/block.html';
-
-    /**
-     * {@inheritDoc}
-     *
-     * @since 4.0.0
-     */
-    public function __construct()
-    {
+class TabBlock extends TabBase {
+
+	/**
+	 * Settings page slug
+	 *
+	 * @since 4.3.2.1
+	 */
+	const SETTINGS_PAGE = 'wp-fail2ban-block';
+
+	/**
+	 * Override Docs link
+	 *
+	 * @since 4.3.2.1
+	 */
+	const HELP_LINK_DOCS = 'https://life-with.wp-fail2ban.com/core/configuration/settings/block/';
+	/**
+	 * Override Reference link
+	 *
+	 * @since 4.3.2.1
+	 */
+	const HELP_LINK_REFERENCE = 'https://docs.wp-fail2ban.com/en/' . WP_FAIL2BAN_VER2 . '/defines/block.html';
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @since 4.0.0
+	 */
+	public function __construct() {
         // phpcs:disable Generic.Functions.FunctionCallArgumentSpacing
-        $this->__['users']              = __('Users',                  'wp-fail2ban');
-        $this->__['user-enumeration']   = __('Block User Enumeration', 'wp-fail2ban');
-        $this->__['blacklist']          = __('Blacklisted Usernames',  'wp-fail2ban');
-        $this->__['username-login']     = __('Block username logins',  'wp-fail2ban');
+		$this->__['users']            = __( 'Users',                  'wp-fail2ban' );
+		$this->__['user-enumeration'] = __( 'Block User Enumeration', 'wp-fail2ban' );
+		$this->__['blacklist']        = __( 'Blacklisted Usernames',  'wp-fail2ban' );
+		$this->__['username-login']   = __( 'Block username logins',  'wp-fail2ban' );
         // phpcs:enable

-        parent::__construct('block', __('Block', 'wp-fail2ban'));
-    }
+		parent::__construct( 'block', __( 'Block', 'wp-fail2ban' ) );
+	}

-    /**
-     * {@inheritDoc}
-     *
-     * @since  4.4.0    Add return type
-     * @since  4.0.0
-     *
-     * @return void
-     */
-    public function admin_init(): void
-    {
-        do_action(__METHOD__.'.before');
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @since  4.4.0    Add return type
+	 * @since  4.0.0
+	 *
+	 * @return void
+	 */
+	public function admin_init(): void {
+		do_action( __METHOD__ . '.before' );

         // phpcs:disable Generic.Functions.FunctionCallArgumentSpacing
-        add_settings_section('wp-fail2ban-users', $this->__['users'],            [$this, 'section'],         self::SETTINGS_PAGE);
-        add_settings_field('user-enumeration',    $this->__['user-enumeration'], [$this, 'userEnumeration'], self::SETTINGS_PAGE, 'wp-fail2ban-users');
-        add_settings_field('blacklist',           $this->__['blacklist'],        [$this, 'users'],           self::SETTINGS_PAGE, 'wp-fail2ban-users');
-        add_settings_field('username-login',      $this->__['username-login'],   [$this, 'usernames'],       self::SETTINGS_PAGE, 'wp-fail2ban-users');
+		add_settings_section( 'wp-fail2ban-users', $this->__['users'],            array( $this, 'section' ),         self::SETTINGS_PAGE );
+		add_settings_field( 'user-enumeration',    $this->__['user-enumeration'], array( $this, 'userEnumeration' ), self::SETTINGS_PAGE, 'wp-fail2ban-users' );
+		add_settings_field( 'blacklist',           $this->__['blacklist'],        array( $this, 'users' ),           self::SETTINGS_PAGE, 'wp-fail2ban-users' );
+		add_settings_field( 'username-login',      $this->__['username-login'],   array( $this, 'usernames' ),       self::SETTINGS_PAGE, 'wp-fail2ban-users' );
         // phpcs:enable

-        do_action(__METHOD__.'.after');
-    }
+		do_action( __METHOD__ . '.after' );
+	}

-    /**
-     * {@inheritDoc}
-     *
-     * @since  4.4.0    Add return type
-     * @since  4.3.3.0  Refactor
-     * @since  4.3.0
-     *
-     * @return void
-     */
-    public function current_screen(): void
-    {
-        $this->add_help_tab('users', [
-            $this->help_entry('user-enumeration', [
-                __('Automated brute-force attacks ("bots") typically start by getting a list of valid usernames ("user enumeration").', 'wp-fail2ban'),
-                __('Blocking user enumeration can force attackers to guess usernames, making these attacks much less likely to succeed.', 'wp-fail2ban'),
-                sprintf(
-                    /* translators: %s: 'Block username logins' */
-                    __('<b>N.B.</b> Some Themes "leak" usernames (for example, via Author profile pages); see %s for an alternative.', 'wp-fail2ban'),
-                    sprintf('<b>%s</b>', $this->__['username-login'])
-                ),
-                $this->see_also(['WP_FAIL2BAN_BLOCK_USER_ENUMERATION'])
-            ]),
-            $this->help_entry('blacklist', [
-                __('Automated brute-force attacks ("bots") will often use well-known usernames, e.g. <tt>admin</tt>.', 'wp-fail2ban'),
-                __('Blacklisted usernames are blocked early in the login process, reducing server load.', 'wp-fail2ban'),
-                $this->see_also(['WP_FAIL2BAN_BLOCKED_USERS'])
-            ]),
-            $this->help_entry('username-login', [
-                __('It is sometimes not possible to block user enumeration (for example, if your theme provides Author profiles). An alternative is to require users to login with their email address.', 'wp-fail2ban'),
-                __('<b>N.B.</b> This also applies to Blacklisted Usernames; you must list <em>email addresses</em>, not usernames.', 'wp-fail2ban'),
-                $this->see_also(['WP_FAIL2BAN_BLOCK_USERNAME_LOGIN'])
-            ])
-        ]);
-
-        parent::current_screen();
-    }
-
-    /**
-     * {@inheritDoc}
-     *
-     * @since  4.4.0    Add return type
-     * @since  4.0.0
-     */
-    public function section(): void
-    {
-        echo '';
-    }
-
-    /**
-     * User Enumeration
-     *
-     * @since  4.4.0    Add return type
-     * @since  4.0.0
-     *
-     * @return void
-     */
-    public function userEnumeration(): void
-    {
-        $this->checkbox('WP_FAIL2BAN_BLOCK_USER_ENUMERATION');
-    }
-
-    /**
-     * Blocked usernames
-     *
-     * @since  4.4.0    Add return type
-     * @since  4.0.0
-     *
-     * @return void
-     */
-    public function users(): void
-    {
-        if (defined('WP_FAIL2BAN_BLOCKED_USERS')) {
-            if (is_array(WP_FAIL2BAN_BLOCKED_USERS)) {
-                $value = join(', ', WP_FAIL2BAN_BLOCKED_USERS);
-            } else {
-                $value = WP_FAIL2BAN_BLOCKED_USERS;
-            }
-        } else {
-            $value = '';
-        }
-        printf(
-            '<input class="regular-text" type="text" disabled="disabled" value="%s">',
-            esc_attr($value)
-        );
-    }
-
-    /**
-     * Block username logins
-     *
-     * @since  4.4.0    Add return type
-     * @since  4.3.0
-     *
-     * @return void
-     */
-    public function usernames(): void
-    {
-        $this->checkbox('WP_FAIL2BAN_BLOCK_USERNAME_LOGIN');
-    }
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @since  4.4.0    Add return type
+	 * @since  4.3.3.0  Refactor
+	 * @since  4.3.0
+	 *
+	 * @return void
+	 */
+	public function current_screen(): void {
+		$this->add_help_tab(
+			'users',
+			array(
+				$this->help_entry(
+					'user-enumeration',
+					array(
+						__( 'Automated brute-force attacks ("bots") typically start by getting a list of valid usernames ("user enumeration").', 'wp-fail2ban' ),
+						__( 'Blocking user enumeration can force attackers to guess usernames, making these attacks much less likely to succeed.', 'wp-fail2ban' ),
+						sprintf(
+							/* translators: %s: 'Block username logins' */
+							__( '<b>N.B.</b> Some Themes "leak" usernames (for example, via Author profile pages); see %s for an alternative.', 'wp-fail2ban' ),
+							sprintf( '<b>%s</b>', $this->__['username-login'] )
+						),
+						$this->see_also( array( 'WP_FAIL2BAN_BLOCK_USER_ENUMERATION' ) ),
+					)
+				),
+				$this->help_entry(
+					'blacklist',
+					array(
+						__( 'Automated brute-force attacks ("bots") will often use well-known usernames, e.g. <tt>admin</tt>.', 'wp-fail2ban' ),
+						__( 'Blacklisted usernames are blocked early in the login process, reducing server load.', 'wp-fail2ban' ),
+						$this->see_also( array( 'WP_FAIL2BAN_BLOCKED_USERS' ) ),
+					)
+				),
+				$this->help_entry(
+					'username-login',
+					array(
+						__( 'It is sometimes not possible to block user enumeration (for example, if your theme provides Author profiles). An alternative is to require users to login with their email address.', 'wp-fail2ban' ),
+						__( '<b>N.B.</b> This also applies to Blacklisted Usernames; you must list <em>email addresses</em>, not usernames.', 'wp-fail2ban' ),
+						$this->see_also( array( 'WP_FAIL2BAN_BLOCK_USERNAME_LOGIN' ) ),
+					)
+				),
+			)
+		);
+
+		parent::current_screen();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @since  4.4.0    Add return type
+	 * @since  4.0.0
+	 */
+	public function section(): void {
+		echo '';
+	}
+
+	/**
+	 * User Enumeration
+	 *
+	 * @since  4.4.0    Add return type
+	 * @since  4.0.0
+	 *
+	 * @return void
+	 */
+	public function userEnumeration(): void {
+		$this->checkbox( 'WP_FAIL2BAN_BLOCK_USER_ENUMERATION' );
+	}
+
+	/**
+	 * Blocked usernames
+	 *
+	 * @since  4.4.0    Add return type
+	 * @since  4.0.0
+	 *
+	 * @return void
+	 */
+	public function users(): void {
+		if ( defined( 'WP_FAIL2BAN_BLOCKED_USERS' ) ) {
+			if ( is_array( WP_FAIL2BAN_BLOCKED_USERS ) ) {
+				$value = join( ', ', WP_FAIL2BAN_BLOCKED_USERS );
+			} else {
+				$value = WP_FAIL2BAN_BLOCKED_USERS;
+			}
+		} else {
+			$value = '';
+		}
+		printf(
+			'<input class="regular-text" type="text" disabled="disabled" value="%s">',
+			esc_attr( $value )
+		);
+	}
+
+	/**
+	 * Block username logins
+	 *
+	 * @since  4.4.0    Add return type
+	 * @since  4.3.0
+	 *
+	 * @return void
+	 */
+	public function usernames(): void {
+		$this->checkbox( 'WP_FAIL2BAN_BLOCK_USERNAME_LOGIN' );
+	}
 }
-
--- a/wp-fail2ban/admin/config/logging.php
+++ b/wp-fail2ban/admin/config/logging.php
@@ -6,235 +6,257 @@
  * @since   4.4.0   Require PHP 7.4
  * @since   4.0.0
  */
-namespace    orglecklidercharleswordpresswp_fail2ban;
+namespace orglecklidercharleswordpresswp_fail2ban;

-defined('ABSPATH') or exit;
+defined( 'ABSPATH' ) or exit;

 /**
  * Tab: Logging
  *
  * @since 4.0.0
  */
-class TabLogging extends TabLoggingBase
-{
-    /**
-     * Settings page slug
-     *
-     * @since 4.3.2.1
-     */
-    const SETTINGS_PAGE = 'wp-fail2ban-logging';
-
-    /**
-     * Override Docs link
-     *
-     * @since 4.3.2.1
-     */
-    const HELP_LINK_DOCS = 'https://life-with.wp-fail2ban.com/core/configuration/settings/logging/';
-    /**
-     * Override Reference link
-     *
-     * @since 4.3.2.1
-     */
-    const HELP_LINK_REFERENCE = 'https://docs.wp-fail2ban.com/en/'.WP_FAIL2BAN_VER2.'/defines/logging.html';
-
-    /**
-     * {@inheritDoc}
-     */
-    public function __construct()
-    {
+class TabLogging extends TabLoggingBase {
+
+	/**
+	 * Settings page slug
+	 *
+	 * @since 4.3.2.1
+	 */
+	const SETTINGS_PAGE = 'wp-fail2ban-logging';
+
+	/**
+	 * Override Docs link
+	 *
+	 * @since 4.3.2.1
+	 */
+	const HELP_LINK_DOCS = 'https://life-with.wp-fail2ban.com/core/configuration/settings/logging/';
+	/**
+	 * Override Reference link
+	 *
+	 * @since 4.3.2.1
+	 */
+	const HELP_LINK_REFERENCE = 'https://docs.wp-fail2ban.com/en/' . WP_FAIL2BAN_VER2 . '/defines/logging.html';
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function __construct() {
         // phpcs:disable Generic.Functions.FunctionCallArgumentSpacing
-        $this->__['what-where']         = __('What & Where',        'wp-fail2ban');
-        $this->__['authentication']     = __('Authentication',      'wp-fail2ban');
-        $this->__['comments']           = __('Comments',            'wp-fail2ban');
-        $this->__['comment-attempts']   = __('Comment Attempts',    'wp-fail2ban');
-        $this->__['spam']               = __('Spam',                'wp-fail2ban');
-        $this->__['password-request']   = __('Password Requests',   'wp-fail2ban');
-        $this->__['pingbacks']          = __('Pingbacks',           'wp-fail2ban');
+		$this->__['what-where']       = __( 'What & Where',        'wp-fail2ban' );
+		$this->__['authentication']   = __( 'Authentication',      'wp-fail2ban' );
+		$this->__['comments']         = __( 'Comments',            'wp-fail2ban' );
+		$this->__['comment-attempts'] = __( 'Comment Attempts',    'wp-fail2ban' );
+		$this->__['spam']             = __( 'Spam',                'wp-fail2ban' );
+		$this->__['password-request'] = __( 'Password Requests',   'wp-fail2ban' );
+		$this->__['pingbacks']        = __( 'Pingbacks',           'wp-fail2ban' );
         // phpcs:enable

-        parent::__construct('logging', __('Logging', 'wp-fail2ban'));
-    }
+		parent::__construct( 'logging', __( 'Logging', 'wp-fail2ban' ) );
+	}

-    /**
-     * {@inheritDoc}
-     *
-     * @since  4.4.0    Add return type
-     * @since  4.0.0
-     *
-     * @return void
-     */
-    public function admin_init(): void
-    {
-        do_action(__METHOD__.'.before');
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @since  4.4.0    Add return type
+	 * @since  4.0.0
+	 *
+	 * @return void
+	 */
+	public function admin_init(): void {
+		do_action( __METHOD__ . '.before' );

         // phpcs:disable Generic.Functions.FunctionCallArgumentSpacing
-        add_settings_section('wp-fail2ban-logging',         $this->__['what-where'],        [$this, 'sectionWhatWhere'],    self::SETTINGS_PAGE);
-        add_settings_field('logging-log-authentication',    $this->__['authentication'],    [$this, 'authentication'],      self::SETTINGS_PAGE,    'wp-fail2ban-logging');
-        add_settings_field('logging-log-comments',          $this->__['comments'],          [$this, 'comments'],            self::SETTINGS_PAGE,    'wp-fail2ban-logging');
-        add_settings_field('logging-log-comment-attempts',  $this->__['comment-attempts'],  [$this, 'commentAttempts'],     self::SETTINGS_PAGE,    'wp-fail2ban-logging');
-        add_settings_field('logging-log-spam',              $this->__['spam'],              [$this, 'spam'],                self::SETTINGS_PAGE,    'wp-fail2ban-logging');
-        add_settings_field('logging-log-password-request',  $this->__['password-request'],  [$this, 'passwordRequest'],     self::SETTINGS_PAGE,    'wp-fail2ban-logging');
-        add_settings_field('logging-log-pingbacks',         $this->__['pingbacks'],         [$this, 'pingbacks'],           self::SETTINGS_PAGE,    'wp-fail2ban-logging');
+		add_settings_section( 'wp-fail2ban-logging',         $this->__['what-where'],        array( $this, 'sectionWhatWhere' ),    self::SETTINGS_PAGE );
+		add_settings_field( 'logging-log-authentication',    $this->__['authentication'],    array( $this, 'authentication' ),      self::SETTINGS_PAGE,    'wp-fail2ban-logging' );
+		add_settings_field( 'logging-log-comments',          $this->__['comments'],          array( $this, 'comments' ),            self::SETTINGS_PAGE,    'wp-fail2ban-logging' );
+		add_settings_field( 'logging-log-comment-attempts',  $this->__['comment-attempts'],  array( $this, 'commentAttempts' ),     self::SETTINGS_PAGE,    'wp-fail2ban-logging' );
+		add_settings_field( 'logging-log-spam',              $this->__['spam'],              array( $this, 'spam' ),                self::SETTINGS_PAGE,    'wp-fail2ban-logging' );
+		add_settings_field( 'logging-log-password-request',  $this->__['password-request'],  array( $this, 'passwordRequest' ),     self::SETTINGS_PAGE,    'wp-fail2ban-logging' );
+		add_settings_field( 'logging-log-pingbacks',         $this->__['pingbacks'],         array( $this, 'pingbacks' ),           self::SETTINGS_PAGE,    'wp-fail2ban-logging' );
         // phpcs:enable

-        do_action(__METHOD__.'.after');
-    }
+		do_action( __METHOD__ . '.after' );
+	}

-    /**
-     * {@inheritDoc}
-     *
-     * @since  4.4.0    Add return type
-     * @since  4.3.3.0  Refactor
-     * @since  4.3.0
-     *
-     * @return void
-     */
-    public function current_screen(): void
-    {
-        $this->add_help_tab('what-where', [
-            $this->help_entry('authentication', [
-                $this->see_also([
-                    'WP_FAIL2BAN_AUTH_LOG'
-                ], false)
-            ]),
-            $this->help_entry('comments', [
-                $this->see_also([
-                    'WP_FAIL2BAN_LOG_COMMENTS',
-                    'WP_FAIL2BAN_LOG_COMMENT_ATTEMPTS',
-                    'WP_FAIL2BAN_COMMENT_ATTEMPT_LOG'
-                ], false)
-            ]),
-            $this->help_entry('spam', [
-                $this->see_also([
-                    'WP_FAIL2BAN_LOG_SPAM',
-                    'WP_FAIL2BAN_SPAM_LOG'
-                ], false)
-            ]),
-            $this->help_entry('password-request', [
-                $this->see_also([
-                    'WP_FAIL2BAN_LOG_PASSWORD_REQUEST',
-                    'WP_FAIL2BAN_PASSWORD_REQUEST_LOG'
-                ], false)
-            ]),
-            $this->help_entry('pingbacks', [
-                $this->see_also([
-                    'WP_FAIL2BAN_LOG_PINGBACKS',
-                    'WP_FAIL2BAN_PINGBACK_LOG'
-                ], false)
-            ])
-        ]);
-
-        parent::current_screen();
-    }
-
-    /**
-     * Section summary.
-     *
-     * @since  4.4.0    Add return type
-     * @since  4.0.0
-     *
-     * @return void
-     */
-    public function sectionWhatWhere(): void
-    {
-        // noop
-    }
-
-    /**
-     * Authentication.
-     *
-     * @since  4.4.0    Add return type
-     * @since  4.0.0
-     *
-     * @return void
-     */
-    public function authentication(): void
-    {
-        printf(
-            '<label>%s: %s</label><p class="description">%s</p>',
-            __('Use facility', 'wp-fail2ban'),
-            $this->getLogFacilities('WP_FAIL2BAN_AUTH_LOG', true),
-            Config::desc('WP_FAIL2BAN_AUTH_LOG')
-        );
-    }
-
-    /**
-     * Comments.
-     *
-     * @since  4.4.0    Add return type
-     * @since  4.0.0
-     *
-     * @return void
-     */
-    public function comments(): void
-    {
-        $this->log(
-            'WP_FAIL2BAN_LOG_COMMENTS',
-            'WP_FAIL2BAN_COMMENT_LOG'
-        );
-    }
-
-    /**
-     * Attempted Comments.
-     *
-     * @since  5.0.0
-     *
-     * @return void
-     */
-    public function commentAttempts(): void
-    {
-        $this->log(
-            'WP_FAIL2BAN_LOG_COMMENT_ATTEMPTS',
-            'WP_FAIL2BAN_COMMENT_ATTEMPT_LOG'
-        );
-    }
-
-    /**
-     * Password request
-     *
-     * @since  4.4.0    Add return type
-     * @since  4.0.0
-     *
-     * @return void
-     */
-    public function passwordRequest(): void
-    {
-        $this->log(
-            'WP_FAIL2BAN_LOG_PASSWORD_REQUEST',
-            'WP_FAIL2BAN_PASSWORD_REQUEST_LOG'
-        );
-    }
-
-    /**
-     * Pingbacks
-     *
-     * @since  4.4.0    Add return type
-     * @since  4.0.0
-     *
-     * @return void
-     */
-    public function pingbacks(): void
-    {
-        $this->log(
-            'WP_FAIL2BAN_LOG_PINGBACKS',
-            'WP_FAIL2BAN_PINGBACK_LOG'
-        );
-    }
-
-    /**
-     * Spam
-     *
-     * @since  4.4.0    Add return type
-     * @since  4.0.0
-     *
-     * @return void
-     */
-    public function spam(): void
-    {
-        $this->log(
-            'WP_FAIL2BAN_LOG_SPAM',
-            'WP_FAIL2BAN_SPAM_LOG'
-        );
-    }
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @since  4.4.0    Add return type
+	 * @since  4.3.3.0  Refactor
+	 * @since  4.3.0
+	 *
+	 * @return void
+	 */
+	public function current_screen(): void {
+		$this->add_help_tab(
+			'what-where',
+			array(
+				$this->help_entry(
+					'authentication',
+					array(
+						$this->see_also(
+							array(
+								'WP_FAIL2BAN_AUTH_LOG',
+							),
+							false
+						),
+					)
+				),
+				$this->help_entry(
+					'comments',
+					array(
+						$this->see_also(
+							array(
+								'WP_FAIL2BAN_LOG_COMMENTS',
+								'WP_FAIL2BAN_LOG_COMMENT_ATTEMPTS',
+								'WP_FAIL2BAN_COMMENT_ATTEMPT_LOG',
+							),
+							false
+						),
+					)
+				),
+				$this->help_entry(
+					'spam',
+					array(
+						$this->see_also(
+							array(
+								'WP_FAIL2BAN_LOG_SPAM',
+								'WP_FAIL2BAN_SPAM_LOG',
+							),
+							false
+						),
+					)
+				),
+				$this->help_entry(
+					'password-request',
+					array(
+						$this->see_also(
+							array(
+								'WP_FAIL2BAN_LOG_PASSWORD_REQUEST',
+								'WP_FAIL2BAN_PASSWORD_REQUEST_LOG',
+							),
+							false
+						),
+					)
+				),
+				$this->help_entry(
+					'pingbacks',
+					array(
+						$this->see_also(
+							array(
+								'WP_FAIL2BAN_LOG_PINGBACKS',
+								'WP_FAIL2BAN_PINGBACK_LOG',
+							),
+							false
+						),
+					)
+				),
+			)
+		);
+
+		parent::current_screen();
+	}
+
+	/**
+	 * Section summary.
+	 *
+	 * @since  4.4.0    Add return type
+	 * @since  4.0.0
+	 *
+	 * @return void
+	 */
+	public function sectionWhatWhere(): void {
+		// noop
+	}
+
+	/**
+	 * Authentication.
+	 *
+	 * @since  4.4.0    Add return type
+	 * @since  4.0.0
+	 *
+	 * @return void
+	 */
+	public function authentication(): void {
+		printf(
+			'<label>%s: %s</label><p class="description">%s</p>',
+			__( 'Use facility', 'wp-fail2ban' ),
+			$this->getLogFacilities( 'WP_FAIL2BAN_AUTH_LOG', true ),
+			Config::desc( 'WP_FAIL2BAN_AUTH_LOG' )
+		);
+	}
+
+	/**
+	 * Comments.
+	 *
+	 * @since  4.4.0    Add return type
+	 * @since  4.0.0
+	 *
+	 * @return void
+	 */
+	public function comments(): void {
+		$this->log(
+			'WP_FAIL2BAN_LOG_COMMENTS',
+			'WP_FAIL2BAN_COMMENT_LOG'
+		);
+	}
+
+	/**
+	 * Attempted Comments.
+	 *
+	 * @since  5.0.0
+	 *
+	 * @return void
+	 */
+	public function commentAttempts(): void {
+		$this->log(
+			'WP_FAIL2BAN_LOG_COMMENT_ATTEMPTS',
+			'WP_FAIL2BAN_COMMENT_ATTEMPT_LOG'
+		);
+	}
+
+	/**
+	 * Password request
+	 *
+	 * @since  4.4.0    Add return type
+	 * @since  4.0.0
+	 *
+	 * @return void
+	 */
+	public function passwordRequest(): void {
+		$this->log(
+			'WP_FAIL2BAN_LOG_PASSWORD_REQUEST',
+			'WP_FAIL2BAN_PASSWORD_REQUEST_LOG'
+		);
+	}
+
+	/**
+	 * Pingbacks
+	 *
+	 * @since  4.4.0    Add return type
+	 * @since  4.0.0
+	 *
+	 * @return void
+	 */
+	public function pingbacks(): void {
+		$this->log(
+			'WP_FAIL2BAN_LOG_PINGBACKS',
+			'WP_FAIL2BAN_PINGBACK_LOG'
+		);
+	}
+
+	/**
+	 * Spam
+	 *
+	 * @since  4.4.0    Add return type
+	 * @since  4.0.0
+	 *
+	 * @return void
+	 */
+	public function spam(): void {
+		$this->log(
+			'WP_FAIL2BAN_LOG_SPAM',
+			'WP_FAIL2BAN_SPAM_LOG'
+		);
+	}
 }
-
--- a/wp-fail2ban/admin/config/plugins.php
+++ b/wp-fail2ban/admin/config/plugins.php
@@ -6,185 +6,172 @@
  * @since   4.4.0   Require PHP 7.4
  * @since   4.2.0
  */
-namespace    orglecklidercharleswordpresswp_fail2ban;
+namespace orglecklidercharleswordpresswp_fail2ban;

-defined('ABSPATH') or exit;
+defined( 'ABSPATH' ) or exit;

 /**
  * Tab: Plugins
  *
  * @since 4.2.0
  */
-class TabPlugins extends TabLoggingBase
-{
-    /**
-     * Settings page slug
-     *
-     * @since 4.3.2.1
-     */
-    const SETTINGS_PAGE = 'wp-fail2ban-plugins';
-
-    /**
-     * {@inheritDoc}
-     */
-    public function __construct()
-    {
-        parent::__construct('plugins', __('Plugins', 'wp-fail2ban'));
-    }
-
-    /**
-     * {@inheritDoc}
-     *
-     * @since  4.4.0    Add return type
-     * @since  4.0.0
-     *
-     * @return void
-     */
-    public function admin_init(): void
-    {
-        do_action(__METHOD__.'.before');
+class TabPlugins extends TabLoggingBase {
+
+	/**
+	 * Settings page slug
+	 *
+	 * @since 4.3.2.1
+	 */
+	const SETTINGS_PAGE = 'wp-fail2ban-plugins';
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function __construct() {
+		parent::__construct( 'plugins', __( 'Plugins', 'wp-fail2ban' ) );
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @since  4.4.0    Add return type
+	 * @since  4.0.0
+	 *
+	 * @return void
+	 */
+	public function admin_init(): void {
+		do_action( __METHOD__ . '.before' );

         // phpcs:disable Generic.Functions.FunctionCallArgumentSpacing
-        add_settings_section('wp-fail2ban-plugins', __('Event Class Facilities', 'wp-fail2ban'), [$this, 'sectionLoggingEventClasses'], self::SETTINGS_PAGE);
-        add_settings_field('plugins-log-auth',      __('Authentication',         'wp-fail2ban'), [$this, 'auth'],                       self::SETTINGS_PAGE, 'wp-fail2ban-plugins');
-        add_settings_field('plugins-log-block',     __('Block',                  'wp-fail2ban'), [$this, 'block'],                      self::SETTINGS_PAGE, 'wp-fail2ban-plugins');
-        add_settings_field('plugins-log-comment',   __('Comment',                'wp-fail2ban'), [$this, 'comment'],                    self::SETTINGS_PAGE, 'wp-fail2ban-plugins');
-        add_settings_field('plugins-log-password',  __('Password',               'wp-fail2ban'), [$this, 'password'],                   self::SETTINGS_PAGE, 'wp-fail2ban-plugins');
-        add_settings_field('plugins-log-rest',      __('REST',                   'wp-fail2ban'), [$this, 'rest'],                       self::SETTINGS_PAGE, 'wp-fail2ban-plugins');
-        add_settings_field('plugins-log-spam',      __('Spam',                   'wp-fail2ban'), [$this, 'spam'],                       self::SETTINGS_PAGE, 'wp-fail2ban-plugins');
-        add_settings_field('plugins-log-xmlrpc',    __('XML‑RPC',                'wp-fail2ban'), [$this, 'xmlrpc'],                     self::SETTINGS_PAGE, 'wp-fail2ban-plugins');
-        add_settings_field('plugins-log-other',     __('Other',                  'wp-fail2ban

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School