Skip to content

3.2.1.5

Latest

Choose a tag to compare

@nilsteampassnet nilsteampassnet released this 07 Aug 10:00
6b43fc5

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_folder only ran when rendering the first page. Asking for start >= 1 skipped 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 carried folder_is_personal, which fed an UPDATE, 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 last TP_ITEMS_LIST_CONTEXT_MAX folders) 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_item verified 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 guards move_item already 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 web move_item, the web mass_move_items (which had its own RSA fan-out) and the API PUT /item/update. Decryption happens before the transaction opens, so the row lock is held only for the redistribution; id_tree, updated_at and 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 through getFolderIdentityWithPersonalFlag(), 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 answers 409. Internal exception messages are no longer returned to the client: they are written to the server log and the client gets a generic 500. Move side effects, the at_moved audit entry, the item cache refresh, the folder counters and the item_moved WebSocket event, now fire for every folder transition, not only personal-to-shared, and a folder_id equal 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 unwritable storage/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_time setting 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_email background task by the create_user_keys final 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 null where a string was expected and the import died on a TypeError. 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, and strict_types rejected the integer, so int|string is 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 .kdbx database 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ès reached the screen as visible markup. A normalizeLogDisplayValue() 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_DATE is unchanged, so an installation already on 3.2.1.4 is not sent through the upgrade wizard.
  • tasks_max_drain_time is 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 that storage/logs is writable.
  • Items list pagination state moved server-side. Anything automated against the do_items_list_in_folder payload 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.

Download TeamPass