Skip to content

fix(auth): compare passwords only through the hash verifier - #38373

Open
freddyDOTCMS wants to merge 8 commits into
mainfrom
issue-901-password-compare-hardening
Open

freddyDOTCMS wants to merge 8 commits into
mainfrom
issue-901-password-compare-hardening

Conversation

@freddyDOTCMS

Copy link
Copy Markdown
Member

Summary

  • Passwords stored in the older digest format are matched only against the digest of the typed password; a stored value is never compared directly with the input. Matching values are upgraded to the current hash on login, as before.
  • Accounts created for SAML, LDAP and CAS sign-in get the hash of a random secret as their local password.
  • The initial admin password (DotCMSInitDb) and the passwords generated in UserManagerImpl.notifyNewUsers are stored hashed.
  • WebDAV sign-in requires the configured pre-authenticator (auth.pipeline.pre) to accept the login, as UserManagerImpl.authenticate already does.
  • Remove the unused UserLocalUtil.
  • UserDataGen stores the hash of the password it is given, so tests sign in the same way users do.

Behaviour change

Accounts whose stored password is plain text no longer sign in with it and need a password reset.

Tests

  • Unit: PasswordFactoryProxyTest (1)
  • Integration, new: LoginFactoryTest (5), DotWebdavHelperAuthorizePrincipalTest (3)
  • Integration, extended: SAMLHelperTest (+1, 13 total)
  • Re-run, all passing: EncryptPlainPasswordsJobTest, UserResourceIntegrationTest, PermissionResourceIntegrationTest, WorkflowResourceLockUnlockIntegrationTest, BinaryExporterServletTest

Suite placement (just suite-timings 6):

MainSuite    median  samples   each run (min)
2a           15.9m       6   16, 16, 13, 13, 17, 19
3a           16.1m       6   17, 18, 12, 16, 16, 16
2b           18.4m       6   20, 17, 20, 19, 17, 17
1a           19.8m       6   19, 20, 17, 17, 21, 20
1b           26.9m       6   23, 29, 28, 21, 27, 26

-> register new coverage in MainSuite2a (15.9 min median)
   slowest suite is MainSuite1b (26.9 min median)

Fixes dotCMS/private-issues#901

🤖 Generated with Claude Code

freddyDOTCMS and others added 2 commits October 29, 2026 16:37
- Stored values in the older digest format are matched only against the
  digest of the typed password; a stored value is never compared directly
  with the input.
- Accounts created for SAML, LDAP and CAS sign-in get the hash of a random
  secret as their local password.
- The initial admin password and the passwords generated for new users are
  stored hashed.
- WebDAV sign-in requires the configured pre-authenticator to accept the
  login, as the portal login already does.
- Remove the unused UserLocalUtil.
- UserDataGen stores the hash of the password it is given.

Fixes dotCMS/private-issues#901

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@freddyDOTCMS freddyDOTCMS added the PR : dotbot review Trigger dotbot AI code review and the post-merge QA test plan label Oct 29, 2026
freddyDOTCMS and others added 5 commits October 30, 2026 08:21
Follow-ups from code review on this branch:

- DotCMSInitDb and UserDataGen mark the account as holding a hash when
  they store one, matching the SAML and OAuth account-creation paths.
- LoginFactory.passwordMatch returns false for an unset stored password
  instead of letting the verifier raise an unchecked exception, which
  surfaced as a server error rather than as failed credentials.
- DotCMSInitDb logs a failure to set the initial admin password instead
  of discarding it; the startup flow is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The client-supplied username and the authenticator's message reached the
log unchanged, so a carriage return or a terminal control sequence in
them altered how the log reads. Route both through the stripLogControls
helper this class already uses for folder paths and hostnames.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The helper derived the Authorization header from user.getPassword(),
which now holds a hash, so the header carried a value no user can sign in
with. Keep the clear text the generator was given and build the header
from that. Verified: 61 tests run, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two conflicts, both resolved by keeping all of each side:

- MainSuite2a: both sides appended to @SuiteClasses. Kept main's five
  entries and re-appended this branch's two at the end.
- UserDataGen: both sides rewrote the persist javadoc. Merged the two
  descriptions; the method body had already merged cleanly, so the
  password hashing and main's updateRememberMeToken call are both present.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review follow-ups on the new coverage:

- SAMLHelperTest provisioned a real user through createNewUser and left
  it behind; remove it in a finally block.
- DotWebdavHelperAuthorizePrincipalTest now extends IntegrationTestBase,
  like the other integration tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@freddyDOTCMS

Copy link
Copy Markdown
Member Author

For review: follow-ups identified during review are tracked as a pending list in the linked issue. Nothing on that list blocks this PR.

One conflict: MainSuite2a. Both sides appended to @SuiteClasses, which is
the end-of-list clash the suite script exists to avoid. Took main's file
whole and re-registered this branch's two classes with
`just add-it-test`, so they land mid-list instead of at the end.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the Area : Backend PR changes Java/Maven backend code label Oct 30, 2026
@claude

claude Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @freddyDOTCMS's task in 3m 8s —— View job


Review

  • Read the PR diff
  • Analyze auth/password changes for bugs & security
  • Check error paths, replay safety, test gaps
  • Post review

I traced the password-storage changes through UserAPIImpl.save (confirmed save(user, systemUser, false) is the 3-arg overload → respectFrontEndRoles, so it does not re-hash — storing the hash directly is correct) and the EncryptPlainPasswordsJob sweep.

New Issues

  • 🟠 High: dotCMS/src/main/java/com/liferay/portal/ejb/UserManagerImpl.java:340 — notifyNewUsers now stores generateHash(newPassword) but never sets user.setPasswordEncrypted(true). The users are loaded via UserUtil.findByC_P(companyId, "password") (line 333) — i.e. accounts whose stored password is the plaintext default, so their passwordEncrypted column/flag is false. After setPassword(hash) the flag stays false, and UserUtil.update(user) persists password_ = hash, passwordEncrypted = false. EncryptPlainPasswordsJob (enabled by default, 0 0/5 * * * ?) selects exactly those rows (passwordEncrypted = false and password_ is not null, EncryptPlainPasswordsJob.java:44) and re-hashes them — producing hash(hash(newPassword)). The clear-text password that was just emailed to the user then fails passwordMatch, locking them out within ~5 minutes.

    This is the one place in the PR that stores a hash without flagging it: DotCMSInitDb.setUpInitialPassword (:203), SAMLHelper.createNewUser (:764) and UserDataGen.persist all set setPasswordEncrypted(true) right after setPassword. The fix is to do the same here.

    Fix this →

Notes (non-blocking)

  • 🟡 Medium: the other random-hash paths — CASAuthUtils.loadUserFromLDAP:125, LoginFactory.syncPassword:287, LoginFactory.doLogin:369, DotCustomLoginPostAction.run:60 — also set a hash without setPasswordEncrypted(true). Here it is harmless (the value is a random secret nobody can type, and the job would only re-hash it into another unmatchable value), so this is not a defect — but setting the flag for consistency would stop EncryptPlainPasswordsJob from repeatedly picking these rows up as "plaintext" on every tick.

The rest of the change is sound:

  • passwordMatch correctly drops the direct equals(password) comparison, guards the typed FAKE_PASSWORD, and guards a null/empty stored value before calling the verifier.
  • The WebDAV change to require Authenticator.SUCCESS from the pre-authenticator matches UserManagerImpl.authenticate (line 564), which also only proceeds to passwordMatch when the PRE pipeline returns SUCCESS.
  • DotCMSInitDb correctly downgrades the init-password failure from an abort to a logged error.

· issue-901-password-compare-hardening

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area : Backend PR changes Java/Maven backend code PR : dotbot review Trigger dotbot AI code review and the post-merge QA test plan

1 participant