Uses the same login comparison everywhere - #487
Conversation
Web server auth and the session guard required a byte exact login match. The login column's collation cannot hold two logins differing only by ASCII case, so that rejected legitimate users without turning away anything the ASCII fold already refuses: an install whose stored login is cased differently from REMOTE_USER lost its session and its login, and a user provisioned from an LDAP entry authenticated once and never again. Also stops a REMOTE_USER that strips to nothing from ending a session, empties the session without relying on Session::isSessionStarted(), which is false whenever Session::start() returned early, and logs the refusal at warning so a locked out user is visible at the default log level.
|
@snake14 Can you check this one ? |
snake14
left a comment
There was a problem hiding this comment.
Looking good. Please check whether these AI items are worth addressing:
Nothing tests the endSession() change. setUp() seeds $_SESSION with only the fingerprint key, which SessionFingerprint::clear() removes anyway, so assertSessionWasEnded() passes with or without the unconditional wipe — I restored the isSessionStarted() gate and the suite stayed 12/12 green. One extra key in setUp() that clear() does not touch would cover it.
A trailing space in REMOTE_USER is still an unrecoverable lockout, in the same shape as the SHIELD\ case. The guard trims only for the emptiness test, then compares the untrimmed value, so 'ironman ' ends the session and WebServerAuth refuses it too — and since REMOTE_USER is non-empty the login form is never reached, so it repeats on every request. Confirmed over real HTTP. Pre-existing rather than introduced here, so happy for it to be a separate ticket, but it is the same class of problem: either trim() once in getAssertedLogin() so both sides agree, or note the residual case in the guard's comment.
Base::isSameLogin() is now a dead seam. Nothing overrides it any more and the other two call sites already use UserIdentity::isSameLogin() directly, so deleting it would make the comparison literally the same everywhere.
Base::makeAuthFailure() still passes null where AuthResult documents a string — the same fix you made in WebServerSessionAuth, and PHPStan already flags it at Auth/Base.php:397.
Minor: the comment above the $_SESSION wipe only holds for the "session already active" case, not the headers_sent() one; and the 5.2.7 changelog heading is missing the space before the dash.
The session wipe was not actually covered: the fixture held only the fingerprint key, which SessionFingerprint::clear() removes anyway, so the assertion passed with or without it. The fixture now carries a namespace clear() leaves alone. getAssertedLogin() trims, so a REMOTE_USER with surrounding whitespace no longer ends the session and refuses the login on every request, and the guard and the auth agree on every value. Base::isSameLogin() had no overrides left and is gone in favour of the one comparison, and makeAuthFailure() passes a string token rather than null.
|
All six addressed in e596d61. Thanks — the first one was the important catch. The Trailing space. Fixed here rather than deferred, since it is an unrecoverable lockout and the fix is one line.
Minor. The wipe comment now only claims the "session already active" case. Changelog heading spacing fixed. Unit suite 164 tests / 387 assertions. The integration tests still cannot run here — no LDAP fixture on this machine, where five pre-existing tests in that file fail on an untouched |
|
Thank you @AltamashShaikh Please check these AI items:
|
getAssertedLogin() trimmed the login it returned, so "ironman " resolved to "ironman" and authenticated it, which is the row the collation returns for it and the comparison exists to refuse. It now returns the value the web server gave and trims only to decide whether anybody was asserted. isCurrentRequestWebServerAuthenticated() still tested REMOTE_USER directly, so an assertion naming nobody reported the request as web server authenticated and skipped Matomo's password confirmation. It and the copy of that test in the token guard now go through the same call.
|
Both blocking items were right — fixed in 5778c30. 1. Trimming the identity was a regression, and my own doing. The previous round flagged the trailing-space case as a lockout and offered "trim once in
That leaves the residual you offered as the alternative: a 2. Fail-open on malformed assertions. Confirmed.
Coverage. 179 tests, 407 assertions. PHPCS clean. |
|
Looking good @AltamashShaikh . Can you please check whether this is really a blocking issue?
|
|
@snake14 |
Addresses review feedback on #484, applied to 5.x first since 5.2.7 is not tagged yet. #484 will be re-ported from this. Refs #AS-689.
The exact match is removed
WebServerAuthand the session guard usedUserIdentity::isSameLoginExact(). The reasoning was that upstream authenticators treat logins differing in ASCII case as separate principals, soREMOTE_USER=karenshould not authenticate as stored superuserKaren.That defence is worth less than it costs.
matomo_user.loginis a primary key under a case- and accent-insensitive collation, sokarenandKarencannot both exist as Matomo users. For the attack to be possible, the upstream would have to treat them as different people while the directory this plugin authenticates against treats them as one entry — LDAPuidmatching is case-insensitive, so it does not. The ASCII fold still refuses everything the collation folds beyond ASCII case, which is the reported issue: a Kelvin-sign identity resolving to an existing local superuser is rejected.Against that, the exact match broke two things:
REMOTE_USER, the first request after the update ended the session and refused the login, superusers included, with no in-app recovery.Base::synchronizeLdapUser()assigns$this->userForLogindirectly, so after synchronizationgetUserForLogin()returns the cached row without re-comparing and the first request succeeded. Every request after that resolved the now-existing row and was refused.Both call sites now use
UserIdentity::isSameLogin(), andisSameLoginExact()is gone.Also fixed
REMOTE_USERthat strips to the empty string (SHIELD\,@shield.org) destroyed a live session:getAssertedLogin()returns''and the guard only tested=== null. Such a value is now treated as no assertion, as it was before this work.endSession()wiped$_SESSIONonly whenSession::isSessionStarted(). That flag stays false wheneverSession::start()returned early — a session already active, or headers sent — while$_SESSIONis populated regardless, sodestroyCurrentSession()carried the previous user's namespaces across. The wipe is now unconditional.logger->debug(), so a locked-out user left no trace at the default level whileUserSynchronizerlogs the equivalent atwarning. It now logs atwarningtoo.new AuthResult(AuthResult::FAILURE, null, null)passes''for both, whichAuthResultdocuments asstring. This is the change Port AS-689 follow-up hardening to 6.x #484 already carries; making it here keeps the branches identical.Tests
tests/Unit/WebServerAuthLoginResolutionTest.phpis new and covers the two lockouts directly — I confirmed both of its positive cases fail when the exact match is reinstated and pass with the fold, and that the three collation-collision cases (accent, Kelvin sign, trailing space) are refused either way.The integration test's
ascii case differencerow moved from the refusal provider to its own passing test, and a provisioning test was added there too. Neither integration test could be run locally — the LDAP fixture is unavailable here, and five pre-existing tests in that file fail for the same reason on an untouched5.x-dev.Plugin unit suite: 160 tests, 364 assertions, OK. PHPCS clean on the changed production files. PHPStan reports nothing in the changed files; the 20 errors it reports across 9 other files are pre-existing on
5.x-dev, which has aphpstan.neonbut no PHPStan workflow.Checklist