By Greg Nowak. Last updated 2026-07-15.
WordPress 6.8 made password storage stronger without asking users to reset their passwords. For an ordinary site using the standard WordPress login, the change is deliberately uneventful. The operational risk sits elsewhere: old plugins, member portals, mobile APIs, migration tools, and support scripts that treat WordPress database fields as a stable authentication interface.
Those integrations may work for months after an upgrade. Then one user logs in through WordPress, their old password hash is replaced, and a separate login bridge stops recognising the account. That delayed failure is why this deserves an audit rather than a quick admin-login check.
What WordPress 6.8 actually changed
Since WordPress 6.8, wp_hash_password() uses bcrypt by default. WordPress first processes the password with SHA-384 to avoid bcrypt’s 72-byte input limit, then stores the resulting hash with a WordPress-specific prefix—normally $wp$2y$.
Existing phpass hashes, usually identified by $P$, remain valid. During the standard username-and-password authentication flow, WordPress checks whether a successful login needs rehashing and saves a new hash when necessary. Users are not migrated in one database operation, so old and new formats can coexist indefinitely on a site with infrequent users.
WordPress also introduced wp_fast_hash() for randomly generated, high-entropy secrets. Application passwords, password-reset keys, personal-data request keys, and recovery-mode keys can consequently appear with a $generic$ prefix. This fast BLAKE2b-based mechanism is not intended for user-chosen passwords.
The problem is the contract, not the prefix
A common patch is to teach an integration about $wp$2y$ and $generic$. That may restore service today, but it preserves the underlying design problem: an internal storage format has become an undocumented contract between systems.
WordPress already knows how to handle its current format, legacy phpass and MD5 values, compatible hashes created by plugins, and deliberately selected alternatives such as Argon2. Custom code should call the relevant WordPress authentication API instead of reproducing that decision tree.
| Authentication path | Warning sign | Preferred boundary | Minimum test |
|---|---|---|---|
| Browser or member login | Reading user_pass directly |
wp_authenticate() |
Login before and after rehashing |
| Custom password check | Calling PHP password_verify() on WordPress data |
wp_check_password() |
Legacy and current hashes |
| Application integration | Inspecting _application_passwords |
REST authentication or WP_Application_Passwords::check_password() |
Old and newly issued credentials |
| Password reset | Reimplementing user_activation_key checks |
check_password_reset_key() |
Issue, validate, expire, and reuse attempts |
| SSO or social login | Fallback code touches stored hashes | The provider callback and WordPress user APIs | Success, denial, logout, and fallback |
Search the codebase before tracing individual bugs
Start with custom plugins, must-use plugins, theme code, deployment utilities, and integration packages. These searches are intentionally broad; a match is a review prompt, not proof of a vulnerability.
rg -n '\$P\$|\$wp\$2y\$|\$generic\$|user_pass|user_activation_key|_application_passwords|PasswordHash' wp-content/
rg -n 'md5\(|password_verify\(|password_hash\(|wp_hash_password|wp_check_password|wp_verify_fast_hash' wp-content/Also search repositories outside the WordPress installation. A Node, Java, .NET, or older PHP service that receives a copy of wp_users is often the real source of the failure. Database exports, reporting replicas, and migration code deserve attention too.
Use the highest-level supported API available
For a conventional username-and-password flow, let WordPress run the authentication pipeline:
$user = wp_authenticate( $username, $password );
if ( is_wp_error( $user ) ) {
return $user;
}
// Authentication succeeded.If custom code already has a user object and genuinely needs a password comparison, use wp_check_password(). Remember that this function only checks the value; it does not itself save a replacement hash. The standard wp_authenticate_username_password() flow performs the rehash after a valid check. A custom flow that bypasses it must deliberately handle that lifecycle, including the session implications of updating a password.
For application passwords, prefer WordPress’s REST authentication rather than reading user metadata. If verification inside WordPress is unavoidable, WP_Application_Passwords::check_password() handles both current and pre-6.8 application-password hashes. Use wp_verify_fast_hash() directly only for an appropriate high-entropy secret hashed through the matching WordPress mechanism—not as a generic password checker.
If another service needs to authenticate WordPress users, exporting password hashes is the fragile option. A small authenticated endpoint, an established identity provider, or a proper SSO boundary keeps the hashing implementation inside the system that owns it.
A staging test that reflects production
Create a test matrix around user journeys, not only functions. Include an account with a legacy hash and confirm its first standard login succeeds. Capture only the hash prefix—not credentials or full hashes—then confirm that the stored format changes and a second login still works.
Repeat the test through every real entry point: member portal, mobile client, customer dashboard, support impersonation tool, SSO fallback, password reset, application password, and recovery flow. Test failure cases as well as success. An integration that accepts an expired reset key or sends useful credentials into logs is a more serious problem than a clean rejection.
Finally, review observability. Authentication logs should identify the route, integration, and error category without recording plaintext passwords, tokens, or complete stored hashes. That gives the operations team enough context to distinguish a proxy problem, disabled application-password feature, rejected SSO assertion, and incompatible legacy bridge.
When a targeted review is worth it
A standard WordPress site probably needs no special project. A site with custom authentication, shared user tables, an inherited membership plugin, or credentials checked by another service does. The useful deliverable is a map of authentication paths, evidence from staging, and a short remediation list—not a speculative rebuild.
If ownership of those paths is unclear, Greg can audit the WordPress and integration code, replace direct hash handling with supported boundaries, and build a focused test plan for your release process. Talk to Greg about a targeted WordPress authentication review.
Related on GrN.dk
- CodeIgniter Tips and Tricks for Secure Login and Password Resets
- NGINX 1.30 changed upstream connection reuse by default: what to check before you upgrade
- When AI writes JSON, one bad field can break the workflow
Need help with this kind of work?
Ask Greg about a WordPress authentication audit Get in touch with Greg.
Sources
- WordPress 6.8 will use bcrypt for password hashing
- wp_check_password() – WordPress Developer Resources
- wp_authenticate_username_password() – WordPress Developer Resources
- wp_verify_fast_hash() – WordPress Developer Resources
- WP_Application_Passwords::check_password() – WordPress Developer Resources