TCHAP: invalidate client on account expiry#1599
Open
thistehneisen wants to merge 1 commit into
Open
Conversation
On ORG_MATRIX_EXPIRED_ACCOUNT the handler called stopMatrixClient(false), which keeps the MatrixClient, its in-memory decrypted room/crypto store and the access token fully live; enforcement was only the expired-account modal. An expired account could therefore still read already-synced decrypted content and reuse the authenticated client. Capture the minimal credentials needed by the account_validity endpoints before teardown, then call stopMatrixClient(true) to unset the client and destroy the in-memory store. requestNewExpiredAccountEmail() and isAccountExpired() no longer depend on a live client (raw fetch using the captured credentials), so the renewal flow keeps working.
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.
Fix: tear down the Matrix client on account expiry instead of keeping it live
Summary
When the homeserver reports an expired account
(
HttpApiEvent.ORG_MATRIX_EXPIRED_ACCOUNT, from thesynapse-email-account-validitymodule),ExpiredAccountHandlercalled:With
unsetClient = false,Lifecycle.stopMatrixClient()only stops the syncloop and React services. It does not call
MatrixClientPeg.unset()orcli.store.destroy(), so after expiry:MatrixClientand its in-memory, already-decrypted room/crypto storestay fully resident and reachable (e.g.
mxMatrixClientPeg.get().getRooms()),and the only thing enforcing expiry is the non-cancelable
ExpiredAccountDialogReact overlay.
Impact
An expired account is meant to be revoked (account-lifecycle / off-boarding).
Because the session is only "paused" and never invalidated client-side, an
expired user can, in the still-open tab:
server calls, and
server-side account-validity module does not gate.
The control is effectively cosmetic. Confidentiality of already-synced
content is the primary impact.
Why it wasn't simply
stopMatrixClient(true)The renewal flow legitimately needs an authenticated call after expiry:
TchapUtils.requestNewExpiredAccountEmail()— POSTaccount_validity/send_mailTchapUtils.isAccountExpired()— re-check used byonBeforeCloseBoth used
MatrixClientPeg.safeGet(), which throws once the client is unset.That coupling is why the client was kept alive — and why the hole existed.
Fix
Decouple the renewal flow from the live client, then tear the client down
properly:
ExpiredAccountHandler.onExpiredAccountError()now captures the minimalcredentials (
homeserverUrl,accessToken,userId) before teardown viaTchapUtils.setExpiredAccountCredentials(...), then callsstopMatrixClient(true)— which unsets the peg and destroys the in-memory(decrypted) room/crypto store.
requestNewExpiredAccountEmail()andisAccountExpired()no longer touch theMatrix client. They use the captured credentials and raw
fetch:isAccountExpired()now does an authenticated GET on the profile endpointand checks the
ORG_MATRIX_EXPIRED_ACCOUNTerrcode (same signal the oldgetProfileInfo()relied on), so it works with no client/store.flow (behaviour unchanged there).
PlatformPeg.get().reload()rebuilds afresh client from the stored session, so the user experience is unchanged.
Residual risk (intentional / out of scope)
This does not clear the persisted session (localStorage/IndexedDB) or revoke
the token server-side — doing so would force a full re-login after every renewal
and is the server's responsibility (the account-validity module). The token is
still held transiently in JS memory while the expiry dialog is open, scoped to
the two
account_validityendpoints, and discarded on reload. The concreteimprovements are: the resident decrypted message/crypto store is destroyed,
the live client and
mxMatrixClientPegaccess are removed, and credentialexposure is reduced to an ephemeral, endpoint-scoped token.
Changes
src/tchap/lib/ExpiredAccountHandler.ts— capture creds before teardown;stopMatrixClient(false)→stopMatrixClient(true).src/tchap/util/TchapUtils.ts— credential holder + setter/getter; rewriterequestNewExpiredAccountEmail()/isAccountExpired()to be client-free;drop now-unused
MatrixErrorimport.src/tchap/util/TchapApi.ts— addprofileUrlconstant.Testing
mxMatrixClientPeg.get()is nownull and
getRooms()/decrypted timelines are no longer reachable.working, logged-in session.
Nils Putnins / OffSeq Cybersecurity
npu@offseq.com / https://offseq.com / https://radar.offseq.com