Validate the password WordPress stores, not a sanitized copy of it - #233
Open
dknauss wants to merge 3 commits into
Open
Validate the password WordPress stores, not a sanitized copy of it#233dknauss wants to merge 3 commits into
dknauss wants to merge 3 commits into
Conversation
validate_strong_password() detects an empty/unchanged password field with `trim( $_POST['pass1'] )`. An all-whitespace value (e.g. " ") trims to '' and is treated as "no password set", so the function returns early and skips the length, zxcvbn, and Have I Been Pwned checks — while WordPress still saves the whitespace password. A user under an enforced strong-password policy can defeat it this way. (The same early-return also fires for a password of "0", which is falsy after trim.) Only a genuinely empty field means "no change", so gate on the string being non-empty instead of on trim().
sanitize_text_field() strips tags, collapses whitespace, and removes octets, so the length, zxcvbn, and Have I Been Pwned checks ran against a string the user never chose and the site never stores. Concretely, "my<b>secret phrase" is validated as "mysecret phrase": a different length, a different zxcvbn score, and a different SHA-1 prefix sent to HIBP. The verdict is real, it just belongs to another password. Core never sanitizes here. edit_user() takes $_POST['pass1'] as-is (rejecting any password containing a backslash), and wp_insert_user() unslashes before hashing, so wp_unslash( $_POST['pass1'] ) is exactly the string that gets hashed. The phpcs:ignore stays, with a reason: not sanitizing is the point.
There was a problem hiding this comment.
Pull request overview
This PR updates the strong-password validation logic to ensure the plugin validates the exact password value WordPress will store (i.e., the unslashed raw password), instead of validating a sanitized/modified copy.
Changes:
- Stop using
trim()as the “password set” sentinel so values like"0"don’t skip validation. - Stop running password strength / breach checks against
sanitize_text_field()output; validatewp_unslash( $_POST['pass1'] )instead. - Add detailed inline rationale and a PHPCS ignore reason explaining why password input must not be sanitized.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
2 tasks
A crafted request can submit pass1 as an array (pass1[]=...). The '' !== $_POST['pass1'] check passes for an array, and (string) then coerces it to the string "Array" (with a PHP array-to-string warning), so the strength and breach checks ran against "Array" rather than treating the input as no password. Read the raw value first and treat any non-string as no password. Addresses the Copilot review note.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Passwords::validate_strong_password()mishandles$_POST['pass1']in two ways, and both mean the policy is applied to something other than the password being set.1. The blank-field test uses
trim()as a boolean. That treats an all-whitespace value (" ") — and, because the trimmed result is evaluated as a boolean, a value of"0"— as "no password set", so the function returns early and skips its zxcvbn and Have I Been Pwned checks.This one is harmless in practice: core already rejects a whitespace-only password on every path, so one cannot actually be set or used through the UI.
edit_user()trimspass1and only savesif ( ! empty( $pass1 ) )→" "becomes a no-op change.reset_password()is called.wp_authenticate()trims the entry andwp_authenticate_username_password()rejects an empty password.So it is not a policy bypass — the blank-field test should simply key off an empty field, so the plugin's own checks do not silently skip.
2. The password is passed through
sanitize_text_field()before validation. This one has teeth. That function strips tags, collapses whitespace, and removes octets, so the length, zxcvbn, and HIBP checks all run against a string the user did not choose and WordPress will not store.Concretely,
my<b>secret phraseis validated asmysecret phrase: a different length, a different zxcvbn score, and a different SHA-1 prefix sent to HIBP. The verdict returned is real — it just belongs to a different password. In practice this surfaces as confusing rejections (a fine password scored as something weaker, or matched against a breach entry the user never used), and it means the breach check does not actually cover the credential that ends up in the database.Core never sanitizes here, and passwords are the standard example of a value that must not be sanitized.
edit_user()takes$_POST['pass1']as-is — rejecting any password containing a backslash — andwp_insert_user()unslashes before hashing, sowp_unslash( $_POST['pass1'] )is exactly the string that gets hashed.Fix
Only a genuinely empty field means "no change", and what gets validated is what gets stored. The
phpcs:ignorestays, now with a reason on it: not sanitizing is the point.Legitimate password changes are unaffected, except that passwords containing markup, doubled spaces, or trailing whitespace are now judged as themselves.
Verification Process
php -l includes/classes/Authentication/Passwords.phpcomposer lint— clean; the four warnings the full run reports are pre-existing ondevelopinAdminCustomizations/EnvironmentIndicator.phpand are untouched here.git diff --checkwp-admin/includes/user.php(edit_user()trim and backslash rejection) andwp_insert_user()'swp_unslash()before hashing.Changelog Entry
Checklist:
AI assistance was used in drafting and reviewing this change; final authorship and verification are mine.