分析 WordPress `wp_check_password()` 函数的源码:如何检查密码是否匹配。

Alright, buckle up everyone, because today we’re diving deep into the murky waters of password verification in WordPress. Specifically, we’re dissecting the wp_check_password() function like a frog in a high school biology class, but hopefully with less formaldehyde and more enlightenment.

We’re going to trace the code, understand its logic, and laugh (maybe cry) at some of the historical quirks along the way. So, grab your coffee, open your text editor (because you should follow along!), and let’s get started.

The Grand Entrance: wp_check_password()

The wp_check_password() function is located in wp-includes/pluggable.php. It’s the gatekeeper, the bouncer at the VIP section of your WordPress installation. Its job is simple: take a plaintext password provided by a user and compare it to the hashed password stored in the database. If they match, the user gets in. If not, they’re stuck outside in the cold, digital rain.

Here’s the basic signature of the function:

function wp_check_password( $password, $hash, $user_id = '' ) {
  // Function body goes here
}

Let’s break down the parameters:

  • $password: This is the plaintext password entered by the user.
  • $hash: This is the hashed password retrieved from the database, associated with the user attempting to log in.
  • $user_id: (Optional) The user ID. This parameter is used for more advanced checks, such as preventing password reuse.

The Initial Assessment: Empty Password?

The first thing wp_check_password() does is check if the password is empty.

if ( empty( $password ) ) {
  /**
   * Filters whether an empty password is valid.
   *
   * @since 3.6.0
   *
   * @param bool $valid Whether an empty password is valid. Default false.
   * @param string $hash  The stored password hash.
   * @param int    $user_id The user ID.
   */
  return apply_filters( 'check_password_empty_password', false, $hash, $user_id );
}

Why do this? Well, an empty password is never a good idea. It’s a huge security risk. WordPress, by default, considers an empty password invalid. However, the check_password_empty_password filter allows developers to override this behavior. (Don’t do it. Seriously.)

The Algorithm Showdown: Identifying the Hash

Now comes the interesting part: figuring out what hashing algorithm was used to create the $hash in the first place. WordPress has evolved its password hashing algorithms over time, so the function needs to be able to handle different formats.

if ( strpos( $hash, '$P$' ) === 0 ) {
  // Older MD5-based hashing (deprecated)
  $check = hash_equals( $hash, wp_hash_password( $password, $hash ) );
} elseif ( strpos( $hash, '$H$' ) === 0 ) {
  // Another older MD5-based hashing (deprecated)
  $check = hash_equals( $hash, wp_hash_password( $password, $hash ) );
} elseif ( strlen( $hash ) == 32 ) {
  // Plain MD5 (very, very old and insecure)
  $check = hash_equals( $hash, md5( $password ) );
} else {
  // Modern password hashing (bcrypt)
  $check = password_verify( $password, $hash );
}

Let’s break down these conditions:

  1. strpos( $hash, '$P$' ) === 0 and strpos( $hash, '$H$' ) === 0: These checks look for older, deprecated MD5-based hashing schemes used in very old versions of WordPress. If found, it attempts to re-hash the provided $password using wp_hash_password() with the existing $hash as a seed and compare the result. These algorithms are known to be vulnerable and should not be used.

  2. strlen( $hash ) == 32: This checks if the length of the hash is 32 characters, which is the length of a standard MD5 hash. If so, it assumes the password was hashed using plain MD5 (without salting or stretching). This is incredibly insecure and a major red flag. WordPress should never store passwords in plain MD5.

  3. else: If none of the above conditions are met, it’s assumed that the password was hashed using the modern bcrypt algorithm, which is what WordPress uses by default since version 4.4. It uses the PHP’s built-in password_verify() function to perform the comparison.

A Word on Insecure Hashing (MD5)

I cannot stress this enough: using MD5 for password hashing is a terrible idea. MD5 is a fast hashing algorithm, which makes it vulnerable to brute-force attacks and rainbow table attacks. Modern hashing algorithms like bcrypt are designed to be slow, making them much more resistant to these attacks.

Modern Hashing: bcrypt and password_verify()

If the hash is identified as a bcrypt hash, the password_verify() function is used:

$check = password_verify( $password, $hash );

password_verify() is a built-in PHP function that securely compares a plaintext password against a bcrypt hash. It handles the complexities of the bcrypt algorithm, including the salt and cost factor.

Why hash_equals() and not ===?

You might notice that in the older hashing checks, hash_equals() is used instead of the standard === operator for string comparison. This is a crucial security precaution.

hash_equals() is designed to prevent timing attacks. A timing attack exploits the fact that string comparisons in PHP can take slightly different amounts of time depending on how many characters match. An attacker could potentially use this timing difference to guess the correct password one character at a time.

hash_equals() mitigates this risk by always taking the same amount of time to compare two strings, regardless of how many characters match.

Updating Old Hashes: Security and Compatibility

WordPress is committed to security and tries to keep up with modern standards. This means that when a user logs in with an old, insecure hash, WordPress attempts to update the hash to the more secure bcrypt algorithm.

if ( $check && $needs_rehash = password_needs_rehash( $hash, PASSWORD_DEFAULT, array( 'cost' => 12 ) ) ) {
  wp_set_password( $password, $user_id );
}

Let’s break this down:

  1. $check: This ensures that the password actually matched the old hash. We don’t want to rehash a password if the user entered the wrong one.

  2. password_needs_rehash( $hash, PASSWORD_DEFAULT, array( 'cost' => 12 ) ): This function checks if the existing hash needs to be updated. It takes the following parameters:

    • $hash: The existing hash.
    • PASSWORD_DEFAULT: This constant specifies the default hashing algorithm to use (which is currently bcrypt).
    • array( 'cost' => 12 ): This specifies the cost factor for the bcrypt algorithm. The cost factor controls the computational effort required to generate the hash. A higher cost factor makes the hashing process slower, making it more resistant to brute-force attacks. A value of 12 is a good balance between security and performance.
  3. wp_set_password( $password, $user_id ): If password_needs_rehash() returns true, this function is called to update the user’s password in the database. It re-hashes the $password using bcrypt with the specified cost factor and saves the new hash.

Filtering the Results: The Power of Plugins

Finally, wp_check_password() applies a filter to the result before returning it.

/**
 * Filters whether the given password is valid for the user.
 *
 * @since 2.5.0
 *
 * @param bool   $check   Whether the password is valid.
 * @param string $password The password in plain text.
 * @param string $hash     The stored password hash.
 * @param int    $user_id  The user ID.
 */
return apply_filters( 'wp_check_password', $check, $password, $hash, $user_id );

This filter, wp_check_password, allows plugins to hook into the password verification process and perform additional checks. For example, a plugin could implement password complexity requirements, two-factor authentication, or prevent the use of compromised passwords.

Code Example: Putting it All Together

Let’s create a simplified example of how wp_check_password() might be used in a login process:

<?php

// Assume we have the username and password from a login form
$username = $_POST['username'];
$password = $_POST['password'];

// 1. Get the user data by username (replace with your actual logic)
$user = get_user_by( 'login', $username );

if ( $user ) {
  // 2. Get the stored password hash
  $hash = $user->data->user_pass;

  // 3. Check the password using wp_check_password()
  $check = wp_check_password( $password, $hash, $user->ID );

  if ( $check ) {
    // 4. Password is valid! Log the user in
    wp_set_auth_cookie( $user->ID );
    wp_redirect( admin_url() ); // Redirect to the admin dashboard
    exit;
  } else {
    // 5. Password is invalid
    echo "Incorrect password.";
  }
} else {
  // 6. User not found
  echo "User not found.";
}

?>

Table Summary: Key Components of wp_check_password()

Component Description Security Implication
Empty Password Check Prevents users from using empty passwords. Enforces a minimum level of security.
Hash Algorithm Detection Identifies the hashing algorithm used to create the stored password hash. Ensures compatibility with older password hashes while prioritizing modern algorithms.
MD5 Hashing (Deprecated) Legacy support for very old and insecure MD5 hashes. Major Security Risk: Should be updated to bcrypt immediately.
bcrypt Hashing Modern password hashing algorithm using password_verify(). Provides strong password security with salting and adaptive hashing.
hash_equals() Secure string comparison to prevent timing attacks. Prevents attackers from exploiting timing differences to guess passwords.
Password Re-hashing Updates old, insecure hashes to bcrypt when a user logs in. Gradually improves the overall security of the WordPress installation.
wp_check_password Filter Allows plugins to extend the password verification process. Enables developers to add custom security measures, such as two-factor authentication.

In Conclusion

The wp_check_password() function is a critical component of WordPress security. It’s responsible for ensuring that only authorized users can access the system. While it handles legacy hashing algorithms, it prioritizes the use of modern, secure algorithms like bcrypt. Understanding how this function works is essential for any WordPress developer who wants to build secure and reliable applications. And remember, folks, always keep your WordPress installation and plugins up to date to benefit from the latest security enhancements! This ensures that the function continues to be robust and effective in the face of evolving security threats. Stay safe out there in the digital wilderness!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注