What's Changed
This is a security and maintenance release on the 3.2.1 line. It closes two authorization bypasses reachable by any authenticated account (one in the items list pagination, one in the item copy destination) rebuilds the personal-to-shared item move on a single hardened path shared by the web interface and the API, and repairs the account creation email that never reached users created by an LDAP first login. Upgrading is recommended for all installations: both advisories are exploitable without any privilege beyond having an account.
This release is code only: no ALTER TABLE and no schema change. UPGRADE_MIN_DATE is deliberately left at its 3.2.1.4 value, so installations already running 3.2.1.4 are not sent back through the upgrade wizard.
🔒 Security fixes
-
Any authenticated user could read the item metadata of any folder through the items list pagination (GHSA-xrjw-r5hx-29rj) - the folder authorization check in
do_items_list_in_folderonly ran when rendering the first page. Asking forstart >= 1skipped it entirely and returned the label, login, description, URL and tags of every item of any folder, including the personal folders of other users. The same request carried its pagination state as a client-supplied blob, and that blob also carriedfolder_is_personal, which fed anUPDATE, so a crafted payload could mark the items of an arbitrary shared folder as personal and make them disappear for everyone else. The authorization check now runs on every page, and the pagination state is kept server-side in the session (bounded to the lastTP_ITEMS_LIST_CONTEXT_MAXfolders) instead of round-tripping through the client: a page beyond the first with no stored context is refused rather than trusted. The breadcrumb is still built before the check, because the interface expects an unauthorized folder to return its path. -
Any non-read-only user could copy an item into any folder, including admin-restricted ones (GHSA-g797-4rh6-m2xp) -
copy_itemverified the user's rights on the source folder and never looked at the destination, so the destination folder id was an unchecked user-controlled key. It now validates the destination against the user's accessible folders and checks the create right, which also rejects a read-only destination, the same pair of guardsmove_itemalready applied.
✨ New features
-
Knowledge base entries are searchable from the command palette - the palette indexed items and folders only, so knowledge base content was reachable only by navigating to its own page. Entries now appear as their own result section, matched on title and description, and gated exactly like the KB menu (feature enabled, non-admin, page allowed). Stored rich text is flattened before matching, so a hit inside the markup is never reported as a hit in the content, and the excerpt is centred on the match. The items and folders lookup became a guarded branch rather than an early return, so a user with no accessible folder now gets knowledge base results instead of an empty palette.
-
The account avatar is shown in the topbar (PR #5321, @guerricv) - the picture uploaded on the profile page was only visible on that page. It now appears in the topbar account chip and in the dropdown account header, resolved from the session (thumbnail first, then the full image), with the initials kept as the fallback when no avatar file exists. Both places refresh immediately after an upload or a deletion. The URL resolution, basename, existence check and encoding, lives in a single
getUserAvatarUrl()used by the topbar and the profile page, and a stale avatar reference pointing at a missing file is ignored rather than rendered broken.
🛠️ Improvements
-
The personal-to-shared item move is now one hardened implementation (PR #5320, @guerricv) - moving an item out of a personal folder has to recover its object keys and redistribute them to every eligible user, and three call sites each did it their own way. They now all call
movePersonalItemToSharedFolderSynchronously(): the webmove_item, the webmass_move_items(which had its own RSA fan-out) and the APIPUT /item/update. Decryption happens before the transaction opens, so the row lock is held only for the redistribution;id_tree,updated_atand the password are revalidated under the lock, and a concurrent change is rejected instead of overwritten. The lock is taken on the item row alone, the previous join also locked the source folder row. The personal flag is resolved throughgetFolderIdentityWithPersonalFlag(), which mirrors the API's own resolution including personal-root containment, so a legacy personal subfolder whose flag was never written no longer makes the caller and the callee disagree.Most importantly, a missing source key now aborts the move instead of destroying data: the previous web path deleted the custom-field row when the moving user had no field sharekey. The item stays personal and the user is told to run the encryption keys repair task. Mass move reports which items were left behind rather than failing as a whole, and the three failure modes, missing key, concurrent change, invalid request, get distinct messages instead of one generic error.
-
The API reports item move failures accurately - a personal-to-shared move combined with another update is refused only when the payload contains a field that would actually be written, so an unknown extra key is no longer treated as a conflict. A missing or unusable key and a validation failure answer
422, a concurrent modification answers409. Internal exception messages are no longer returned to the client: they are written to the server log and the client gets a generic500. Move side effects, theat_movedaudit entry, the item cache refresh, the folder counters and theitem_movedWebSocket event, now fire for every folder transition, not only personal-to-shared, and afolder_idequal to the current one is treated as a no-op rather than a move. -
The background task queue is visible on the Health page - a task left pending well past the drain window means the handler is no longer being woken up, usually a missing cron entry,
exec()disabled, or an unwritablestorage/logs. The symptom was silent: notification emails never left, caches never rebuilt, and nothing on screen said so. The Health page now warns when tasks have been queued for more than 15 minutes. -
The task drain window is configurable - how long one run of the background handler keeps launching new tasks was a hard-coded 55 seconds. It is now the
tasks_max_drain_timesetting on the Tasks page (10–3600 s, bounded on both the client and the server). Raise it on large vaults where key generation is slow, so tasks queued by a running task - notification emails, typically are still picked up in the same run. -
The published advisories are listed in
SECURITY.md- each disclosed CVE is now paired with its GHSA identifier, a one-line summary, the version that fixed it and a link to the advisory.
🐛 Bug fixes
-
The account creation email was never sent to users created by their first LDAP login - the email was correctly queued as a
send_emailbackground task by thecreate_user_keysfinal step, but nothing woke the handler up afterwards. The task was only picked up if the handler happened to still be inside its launching window, so on any vault large enough for key generation to exceed the drain window the email stayed queued until the next cron tick, or forever where no cron was configured. Users received it only if a later login happened to trigger the handler again.prepareSendingEmail()now triggers the handler itself. Along the way: the LDAP notification body was rewritten and given its own subject, since unlike the local-account email it carries no credential; the creation paths now pass the language key instead of the already-translated text, so the worker resolves the message in the recipient's own language rather than the creator's; a#firstname#placeholder joins the legacy#lastname#; and the interface tells the user to sign in again instead of only promising an email that may take a moment. -
KeePass XML import failed on entries with empty fields - an entry without a title, URL or username produced
nullwhere a string was expected and the import died on aTypeError. Those reads are now guarded. A second failure hit the import at the root: the parent folder identifier is the integer destination folder on the first call and a KeePass group UUID string in recursion, andstrict_typesrejected the integer, soint|stringis now accepted. The upload error toast also shows the name of the failing file instead of the configured maximum size, matching the manager uploader, and a hint on the import page states that the KeePass 2.x XML export is what to upload, the encrypted.kdbxdatabase is not supported. -
Legacy HTML entities were displayed literally in the Utilities logs (PR #5316, @guerricv) - values written by older versions are stored entity-encoded, and the log tables escaped them again, so
accèsreached the screen as visible markup. AnormalizeLogDisplayValue()decodes the legacy layers before the value is escaped once for display. It lives in a database-free module so the unit tests exercise the production function rather than a copy of it. The authentication lockout identifier is no longer decoded client-side, so the value shown is exactly the one the unlock action targets, and attribute values get a dedicated escaper,text().html()leaves quotes untouched.
⬆️ Upgrade notes
- Schema. No
ALTER TABLE, no new table, no data migration.UPGRADE_MIN_DATEis unchanged, so an installation already on 3.2.1.4 is not sent through the upgrade wizard. tasks_max_drain_timeis seeded only by the installer and the upgrade script. Because the wizard does not run for 3.2.1.4 installations, the setting row will simply not exist there, the handler and the Tasks page both fall back to the previous hard-coded 55 seconds, and the row is created the first time you save a value on the Tasks page. Nothing to do unless you want to change it.- The new Health page check may raise a warning immediately. If it reports queued tasks older than 15 minutes, that is a pre-existing condition it is now surfacing, not a regression introduced by this release, check your cron entry, that
exec()is not disabled, and thatstorage/logsis writable. - Items list pagination state moved server-side. Anything automated against the
do_items_list_in_folderpayload must go through the first page of a folder before requesting later pages; a page beyond the first with no server-side context is now refused. - Re-check custom-field encryption keys before moving personal items. A personal-to-shared move whose source keys cannot be recovered is now refused instead of silently dropping the field, the affected items stay personal and are reported. Run the encryption keys repair task if you see them.
- Back up your database before upgrading, as always.
Full Changelog: 3.2.1.4...3.2.1.5
Important
- Requires at least
PHP 8.2
Languages
Please join Teampass v3 translation project on Poeditor and translate it for your language.
Installation
Follow instructions from Documentation.
Upgrade
Follow instructions from Documentation.
Ideas and comments
Are welcome ... please use Discussions.