From 119380839d8cd35cf8fbe79db82f8cd7d52b4555 Mon Sep 17 00:00:00 2001 From: Iain Date: Thu, 25 Jun 2026 11:55:19 +0100 Subject: [PATCH 1/3] Add generic login errors setting to prevent username enumeration Adds a new "Generic Login Errors" option under Login Settings that replaces specific error messages (invalid username, incorrect password) with a single generic message, preventing attackers from enumerating valid usernames. Co-Authored-By: Claude Opus 4.6 --- includes/admin/class-wpum-options-panel.php | 6 ++++ includes/filters.php | 39 +++++++++++++++++++++ tests/e2e/login.spec.ts | 33 ++++++++++++++++- 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/includes/admin/class-wpum-options-panel.php b/includes/admin/class-wpum-options-panel.php index 89115f6e..7d4d1551 100644 --- a/includes/admin/class-wpum-options-panel.php +++ b/includes/admin/class-wpum-options-panel.php @@ -244,6 +244,12 @@ public function register_settings( $settings ) { 'value' => true, ), ), + array( + 'id' => 'generic_login_errors', + 'name' => __( 'Generic Login Errors', 'wp-user-manager' ), + 'desc' => __( 'Replace specific login error messages with a generic message to prevent username enumeration.', 'wp-user-manager' ), + 'type' => 'checkbox', + ), ), 'misc' => array( array( diff --git a/includes/filters.php b/includes/filters.php index 4f78b049..d85f49d9 100644 --- a/includes/filters.php +++ b/includes/filters.php @@ -182,6 +182,45 @@ function wpum_authentication( $wp_user, $username, $password ) { } add_filter( 'authenticate', 'wpum_authentication', 20, 3 ); +/** + * Replace specific login error messages with a generic message + * to prevent username enumeration when the setting is enabled. + * + * @param WP_User|WP_Error $user + * @param string $username + * @param string $password + * + * @return WP_User|WP_Error + */ +function wpum_generic_login_errors( $user, $username, $password ) { + if ( ! is_wp_error( $user ) || empty( $username ) ) { + return $user; + } + + if ( ! wpum_get_option( 'generic_login_errors' ) ) { + return $user; + } + + $code = $user->get_error_code(); + + $enumeration_codes = array( + 'invalid_username', + 'invalid_email', + 'incorrect_password', + 'email_only', + ); + + if ( in_array( $code, $enumeration_codes, true ) ) { + return new WP_Error( + 'authentication_failed', + __( 'Error: The username or password you entered is incorrect.', 'wp-user-manager' ) + ); + } + + return $user; +} +add_filter( 'authenticate', 'wpum_generic_login_errors', 50, 3 ); + /** * Highlight all pages used by WPUM. * diff --git a/tests/e2e/login.spec.ts b/tests/e2e/login.spec.ts index 2bcf711c..ac922612 100644 --- a/tests/e2e/login.spec.ts +++ b/tests/e2e/login.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from './fixtures'; +import { test, expect, wpCli } from './fixtures'; test.describe('Login Form', () => { test.beforeEach(async ({ page }) => { @@ -108,6 +108,37 @@ test.describe('Login Form', () => { await expect(page).toHaveURL(/wp-login\.php/); }); + test('generic login errors hides username enumeration', async ({ page, loginPage }) => { + wpCli(`eval 'wpum_update_option("generic_login_errors", true);'`); + + await page.goto(loginPage); + await page.locator('#username').fill('nonexistent_user_12345'); + await page.locator('#password').fill('WrongPassword123!'); + await page.locator('input[name="submit_login"]').click(); + await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {}); + + const errorMessage = page.locator('.wpum-message.error'); + await expect(errorMessage).toBeVisible({ timeout: 5000 }); + const errorText = await errorMessage.textContent(); + expect(errorText).toContain('username or password you entered is incorrect'); + expect(errorText).not.toContain('not registered'); + expect(errorText).not.toContain('Unknown'); + + await page.goto(loginPage); + await page.locator('#username').fill('testuser_login'); + await page.locator('#password').fill('WrongPassword123!'); + await page.locator('input[name="submit_login"]').click(); + await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {}); + + const errorMessage2 = page.locator('.wpum-message.error'); + await expect(errorMessage2).toBeVisible({ timeout: 5000 }); + const errorText2 = await errorMessage2.textContent(); + expect(errorText2).toContain('username or password you entered is incorrect'); + expect(errorText2).not.toContain('incorrect password'); + + wpCli(`eval 'wpum_update_option("generic_login_errors", false);'`); + }); + test('redirect after login', async ({ page, loginPage }) => { await page.goto(loginPage); From f6f6f958d01ada301ec16e22deac61de3d7272bc Mon Sep 17 00:00:00 2001 From: Iain Date: Thu, 25 Jun 2026 11:59:07 +0100 Subject: [PATCH 2/3] Rename setting label to Generic Login Error (singular) Co-Authored-By: Claude Opus 4.6 --- includes/admin/class-wpum-options-panel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/admin/class-wpum-options-panel.php b/includes/admin/class-wpum-options-panel.php index 7d4d1551..25760f2b 100644 --- a/includes/admin/class-wpum-options-panel.php +++ b/includes/admin/class-wpum-options-panel.php @@ -246,7 +246,7 @@ public function register_settings( $settings ) { ), array( 'id' => 'generic_login_errors', - 'name' => __( 'Generic Login Errors', 'wp-user-manager' ), + 'name' => __( 'Generic Login Error', 'wp-user-manager' ), 'desc' => __( 'Replace specific login error messages with a generic message to prevent username enumeration.', 'wp-user-manager' ), 'type' => 'checkbox', ), From 8359b1b5e3b8ab485f6c2255984f35d7021f669f Mon Sep 17 00:00:00 2001 From: Iain Date: Thu, 25 Jun 2026 12:03:37 +0100 Subject: [PATCH 3/3] Wrap generic login error message in wpum_generic_login_error_message filter Co-Authored-By: Claude Opus 4.6 --- includes/filters.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/filters.php b/includes/filters.php index d85f49d9..32a70ad4 100644 --- a/includes/filters.php +++ b/includes/filters.php @@ -213,7 +213,7 @@ function wpum_generic_login_errors( $user, $username, $password ) { if ( in_array( $code, $enumeration_codes, true ) ) { return new WP_Error( 'authentication_failed', - __( 'Error: The username or password you entered is incorrect.', 'wp-user-manager' ) + apply_filters( 'wpum_generic_login_error_message', __( 'Error: The username or password you entered is incorrect.', 'wp-user-manager' ) ) ); }