Password change on an SSO account writes a login block the login flow never looks for — account becomes unreachable
- Dominant language
- JavaScript
- Stars
- 27
- Forks
- 7
- PR merge metrics
- No merged PRs in 30d
Description
Changing the CryptPad password of an SSO account writes a login block that the login flow never looks for. The old block is archived in the same operation, so the account becomes unreachable — with a success message and nothing in the logs to indicate a problem.
Recovered here only from a filesystem snapshot taken minutes before. On a zero-knowledge service the operator cannot restore an account, so this is silent data loss.
## Versions
- CryptPad `2025.9.0` (nixpkgs package, unpatched upstream code)
- `cryptpad/sso` at `4f77fca4a9e937fbbc7c189da41fc126133c641a`
- IdP: Authentik, OIDC, `enforced: true`, `cpPassword: true`, `forceCpPassword: true`
- Single account on the instance, logged in via SSO, `httpSafeOrigin` set to a separate domain (no trailing-slash problem as in #9)
## What happens
Three different block addresses are involved (truncated to 8 chars):
| step | block |
|---|---|
| account before the change | `jlf7ZF+X…` |
| the change writes | `NURx27ND…` — `BLOCK_WRITE_BY_OWNER`, `isChange: true` |
| something then requests | `pABQSODm…` → **404** |
Server log for the change itself:
```
["INFO",…,"BLOCK_WRITE_BY_OWNER",{"blockId":"NURx27ND…","isChange":true,"previousKey":"jlf7ZF+X…"}]
["INFO",…,"ARCHIVAL_BLOCK_BY_OWNER_RPC",{"publicKey":"jlf7ZF+X…","status":"SUCCESS"}]
```
Access log shortly after:
```
GET /block/pA/pABQSODm… 404
```
After this, the account cannot be logged into with either the old or the new password.
## What I ruled out (measured, so you don't have to)
1. **Fallback to the account name.** `settings/inner.js:694` uses `ssoSeed || privateData.accountName`. If the seed had been missing, the derived `oldBytes` would have been wrong and `Q_SETTINGS_CHECK_PASSWORD` would have aborted before writing anything. A block *was* written, so the old password verified correctly — the name used for derivation was right.
2. **Lowercasing mismatch.** `ssoauth/main.js:65` stores the seed as `seed.toLowerCase()`, and `common-login.js:294` lowercases the name on every login ("Usernames are all lowercase. No going back on this one"). Verified on the instance: the seed stored server-side contains uppercase characters, the value in `localStorage['SSO_seed']` contains only digits and lowercase — i.e. exactly the lowercased form, as intended. Both paths agree.
3. **Different derivation.** `deriveBytes` (`settings/inner.js:445-447`) calls `Cred.deriveFromPassphrase(name, password, Login.requiredBytes, cb)` — the same function with the same parameters as `common-login.js:315`.
So the derivation input appears correct at change time (the old-password check passes), yet the resulting block is not where the next login looks. I could not narrow it down further without instrumenting the client.
## Why this matters more than a normal bug
`forceCpPassword: true` is what makes the setup zero-knowledge — the server holds the seed, the user's password is the part it does not know. Users who take that seriously are exactly the ones likely to rotate their password, and the operation destroys their account while reporting success.
If the cause is not obvious, hiding the "change password" section for SSO accounts (the branch at `settings/inner.js:615` already does this when `sso.password === 0`) would at least prevent the loss until it is understood.
Happy to run further tests against this instance if that helps.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by tracing the password-change path in settings/inner.js, especially lines 615 and 694, alongside SSO seed handling in ssoauth/main.js:65 and login derivation in common-login.js:294-315. Reproduce the reported block write and subsequent 404, then compare the derived block addresses used by the change and login flows. Done means an enforced SSO account can change its password and log in afterward, or the change is unavailable when it cannot be made safe.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- authentication, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100