From 3c16f52130e96fa8a69ceb08aa080b36b85ead01 Mon Sep 17 00:00:00 2001 From: Piyush <296399266+skikken@users.noreply.github.com> Date: Mon, 10 Aug 2026 11:32:37 +0530 Subject: [PATCH 1/2] fix: stripslash passwords on login and user edit Passwords containing quotes or backslashes were hashed with magic quote slashes intact on the login, admin user edit, and password reset paths, so the stored hash did not match the actual password. Unslash passwords at the input boundary and migrate legacy hashes on authentication by checking the slashed variant and rehashing. Closes #13655 --- src/wp-admin/includes/user.php | 4 ++-- src/wp-includes/user.php | 28 +++++++++++++++++++++++++++- src/wp-login.php | 4 ++-- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/wp-admin/includes/user.php b/src/wp-admin/includes/user.php index 2aeeba0b035b4..805802d4940d5 100644 --- a/src/wp-admin/includes/user.php +++ b/src/wp-admin/includes/user.php @@ -49,10 +49,10 @@ function edit_user( $user_id = 0 ) { $pass1 = ''; $pass2 = ''; if ( isset( $_POST['pass1'] ) ) { - $pass1 = trim( $_POST['pass1'] ); + $pass1 = trim( wp_unslash( $_POST['pass1'] ) ); } if ( isset( $_POST['pass2'] ) ) { - $pass2 = trim( $_POST['pass2'] ); + $pass2 = trim( wp_unslash( $_POST['pass2'] ) ); } if ( isset( $_POST['role'] ) && current_user_can( 'promote_users' ) && ( ! $user_id || current_user_can( 'promote_user', $user_id ) ) ) { diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index 76bce3966feb2..34f2df8511a61 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -52,7 +52,7 @@ function wp_signon( $credentials = array(), $secure_cookie = '' ) { $credentials['user_login'] = wp_unslash( $_POST['log'] ); } if ( ! empty( $_POST['pwd'] ) && is_string( $_POST['pwd'] ) ) { - $credentials['user_password'] = $_POST['pwd']; + $credentials['user_password'] = wp_unslash( $_POST['pwd'] ); } if ( ! empty( $_POST['rememberme'] ) ) { $credentials['remember'] = $_POST['rememberme']; @@ -207,6 +207,19 @@ function wp_authenticate_username_password( $valid = wp_check_password( $password, $user->user_pass, $user->ID ); + if ( ! $valid ) { + /* + * Back-compat for users whose password was hashed with magic-quote slashes + * intact. If the slashed version matches, migrate the hash to the + * unslashed password. + */ + $valid = wp_check_password( wp_slash( $password ), $user->user_pass, $user->ID ); + + if ( $valid ) { + wp_set_password( $password, $user->ID ); + } + } + if ( ! $valid ) { return new WP_Error( 'incorrect_password', @@ -290,6 +303,19 @@ function wp_authenticate_email_password( $valid = wp_check_password( $password, $user->user_pass, $user->ID ); + if ( ! $valid ) { + /* + * Back-compat for users whose password was hashed with magic-quote slashes + * intact. If the slashed version matches, migrate the hash to the + * unslashed password. + */ + $valid = wp_check_password( wp_slash( $password ), $user->user_pass, $user->ID ); + + if ( $valid ) { + wp_set_password( $password, $user->ID ); + } + } + if ( ! $valid ) { return new WP_Error( 'incorrect_password', diff --git a/src/wp-login.php b/src/wp-login.php index 0b1d2cff308ec..932a162ff1dc7 100644 --- a/src/wp-login.php +++ b/src/wp-login.php @@ -970,7 +970,7 @@ function wp_login_viewport_meta() { // Check if password is one or all empty spaces. if ( ! empty( $_POST['pass1'] ) ) { - $_POST['pass1'] = trim( $_POST['pass1'] ); + $_POST['pass1'] = trim( wp_unslash( $_POST['pass1'] ) ); if ( empty( $_POST['pass1'] ) ) { $errors->add( 'password_reset_empty_space', __( 'The password cannot be a space or all spaces.' ) ); @@ -978,7 +978,7 @@ function wp_login_viewport_meta() { } // Check if password fields do not match. - if ( ! empty( $_POST['pass1'] ) && trim( $_POST['pass2'] ) !== $_POST['pass1'] ) { + if ( ! empty( $_POST['pass1'] ) && trim( wp_unslash( $_POST['pass2'] ) ) !== $_POST['pass1'] ) { $errors->add( 'password_reset_mismatch', __( 'Error: The passwords do not match.' ) ); } From 372bda7c0b39ced4c4dd1086b0d634b9a6fa7bfa Mon Sep 17 00:00:00 2001 From: Piyush <296399266+skikken@users.noreply.github.com> Date: Mon, 10 Aug 2026 11:32:42 +0530 Subject: [PATCH 2/2] test: cover password stripslashing and hash migration --- tests/phpunit/tests/auth.php | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/phpunit/tests/auth.php b/tests/phpunit/tests/auth.php index a290d11e118e6..7b8997a7b874b 100644 --- a/tests/phpunit/tests/auth.php +++ b/tests/phpunit/tests/auth.php @@ -154,6 +154,55 @@ public function data_passwords_for_trimming() { ); } + /** + * Tests that wp_signon() unslashes the password from $_POST. + * + * @ticket 13655 + */ + public function test_wp_signon_unslashes_password() { + $password = "pa'ss"; + wp_set_password( $password, $this->user->ID ); + + $_POST['log'] = $this->user->user_login; + $_POST['pwd'] = wp_slash( $password ); + $_POST['test'] = 1; + + $authed_user = wp_signon(); + + unset( $_POST['log'], $_POST['pwd'], $_POST['test'] ); + + $this->assertNotWPError( $authed_user ); + $this->assertInstanceOf( 'WP_User', $authed_user ); + $this->assertSame( $this->user->ID, $authed_user->ID ); + } + + /** + * Tests that a password hashed with slashes intact is migrated on login. + * + * @ticket 13655 + */ + public function test_slashed_password_hash_is_migrated_on_login() { + $password = "pa'ss"; + + // Simulate a legacy hash of the slashed password. + $slashed_hash = wp_hash_password( wp_slash( $password ) ); + wp_update_user( + array( + 'ID' => $this->user->ID, + 'user_pass' => $slashed_hash, + ) + ); + + $authed_user = wp_authenticate( $this->user->user_login, $password ); + + $this->assertNotWPError( $authed_user ); + $this->assertSame( $this->user->ID, $authed_user->ID ); + + // The hash should now be of the unslashed password. + $user = get_user_by( 'id', $this->user->ID ); + $this->assertTrue( wp_check_password( $password, $user->user_pass, $user->ID ) ); + } + /** * Tests hooking into wp_set_password(). *