post_bulk_access_collections authorized each requested collection via the
legacy Collection::is_manageable_by_user helper, which also accepts a member's
membership/group access_all. For a Custom member this diverged from the
single-collection edit endpoint (ManagerHeaders -> collection_edit_access),
which requires a real per-collection Manage grant and never treats a Custom
member's access_all as one. A flagless Custom member placed in an access_all
group could therefore rewrite collection user/group assignments in bulk while
the single-collection edit endpoint denied the exact same change.
Add auth::can_edit_collection - the collection_edit_access + can_manage_collection
pair the ManagerHeaders guard already uses - and call it per collection in the
bulk-access endpoint. Legacy Managers, Admins/Owners and Edit-any-collection
members are unaffected; only a Custom member's access_all shortcut is removed,
so bulk-access now enforces exactly what the single-collection edit endpoint does.
A Custom member holding only `edit_any_collection` (whose `access_all` mirror
makes every collection "manageable") could grant a per-collection `manage` row
to a group it belongs to -- via post_bulk_access_collections,
post_organization_collection_update, add_update_group or edit_member -- and then
delete that collection through CollectionDeleteHeaders'
`has_explicit_collection_manage_access` path, despite holding no
`delete_any_collection` permission. This defeated the intended edit != delete
separation (confirmed end-to-end: the delete returned 200 before this change).
Gate every caller-controlled collection-assignment `manage` write behind a new
`caller_may_grant_collection_manage()` check that mirrors the delete
authorization exactly: a caller may confer `manage` (which carries delete
authority) on a collection only if it could delete that collection itself --
Admin/Owner or `delete_any_collection` always; an exact legacy Manager via its
per-collection manage helper; any other Custom member only with a real persisted
`users_collections.manage` / `collections_groups.manage` grant.
`edit_any_collection`'s `access_all` mirror deliberately does not count.
The change is strictly subtractive: it can only ever downgrade a requested
`manage` to false, never grant it, so it opens no new access and leaves
Admins/Owners and genuinely delete-capable members unaffected. The create,
organization-import and invite paths are intentionally left alone (new-collection
creators must manage their own collection; non-owner invites can only create
plain Users, which cannot delete).
Adds a unit test for the collection-independent part of the gate.
Follow-up hardening on the custom-role work, found during a static review:
- Admin panel type change (apply_membership_type_change): when converting a
Custom member to the legacy Manager role, only preserve `access_all` if the
member actually held the full "manage all collections" grant (all three
collection flags). Previously an Edit-any-collection-only Custom member
(whose access_all is just the Edit mirror) became a Manager with a broad
access_all grant, silently escalating Edit-only into Create + Edit + Delete.
Mirrors the collection-permissions down-migration. Unit test updated.
- Bulk collection access (post_bulk_access_collections): drop the blanket
has_full_access() requirement and rely on the existing per-collection
is_manageable_by_user check (as the single-collection endpoint and the
pre-existing behavior do). Pure manage_users / manage_groups / manage_policies
Custom members hold no per-collection manage grant and are still rejected,
while a Manager/Custom member who manages some collections regains the ability
to bulk-edit exactly those collections.
- Group details (GET /organizations/<org>/groups/<id>/details): align the guard
with the list endpoint (/groups/details) to ManageUsersOrGroups, so a
manage_users member is not denied the single-group view of the same data it
can already read in bulk.
Addresses the confirmed High/Medium findings of the granular-collection-
permissions security audit:
- H-1: bind direct (non-sync) cipher access to a confirmed membership in the
cipher's organization, and harden the user/group collection access-flag
queries to require org consistency plus confirmed status. Revoked/invited
members can no longer reach ciphers via stale assignment rows.
- H-2: validate group ids against the org before mutating group membership in
edit_member, and reject cross-org group<->membership links in GroupUser::save
and Group::is_in_full_access_group.
- H-3: fully pre-validate collections/groups/users in bulk-access and collection
create before any mutation, and reject cross-org collection<->group links in
CollectionGroup::save.
- M-1: require Manage Users for the full member list and Manage Users/Groups for
group details (new ManageUsersOrGroupsHeaders guard).
- M-2: validate the whole bulk-access request before the destructive
delete/replace so an invalid element can't leave partial state behind.
- M-3: bounds-validate import collection relationships before writing and
propagate cipher-save errors instead of discarding them.
A Custom member with the manage_users permission reaches edit_member via
ManageUsersHeaders. Every mutated field there (access_all, the manage_* flags,
collection and group assignments) is gated behind an Admin/collection-management
check -- except the role type itself, which was written unconditionally aside
from the existing Admin/Owner-elevation guard.
Because collection "manage" rights are role-derived (`atype >= Manager` grants
manage on any collection the member can write), a manage_users caller with no
collection access could promote a plain User to Manager/Custom to grant them
collection administration (rename/delete/re-share), or demote to revoke it --
a separation-of-duties break between the user-management and data planes.
Add a may_change_member_type() guard so callers below Admin may only submit an
unchanged role (keeping the regular edit dialog working), and cover it with a
regression unit test.
* Misc updates and fixes
- Updated Rust to v1.96.1
- Updated all the crates
- Updated GitHub Actions
- Updated the web-vault to v2026.6.2
- Updated Alpine to v3.24
- Fixed several clippy lints
- The `send` UUID wrappers didn't need the special namespace anymore since an updated crate, so removed this extra mod.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update MSRV to v1.94.1
Signed-off-by: BlackDex <black.dex@gmail.com>
---------
Signed-off-by: BlackDex <black.dex@gmail.com>
A Custom member with only the manage_groups permission could revoke other
members' collection access via two endpoints that were missing the
collection-access check enforced elsewhere (put_group_members, edit_member):
- POST /organizations/<org>/groups/<group>/delete-user/<member>
(post_delete_group_member) removed a member from any group, including
collection-bearing ones.
- DELETE /organizations/<org>/groups/<group> and its bulk variant
(delete_group_impl / bulk_delete_groups) deleted collection-bearing groups
outright, revoking access for all their members.
Neither path can grant access, so confidentiality was never at risk, but both
let a manage_groups-only user tamper with other members' collection access,
contradicting the permission's invariant. Both now require Admin/Owner or full
collection access before touching a group that confers collection access (via
access_all or assigned collections).
send_invite computed access_all from the invite permissions but never
wrote the manage_users / manage_groups / manage_policies flags onto the
new membership, so a Custom member invited with a management permission
checked was created without it (the web-vault sends everything in a
single invite POST, with no follow-up edit). Mirror the handling in
edit_member and persist the flags at invite time.
Only Owners can invite Custom members, so the caller is always
authorized to grant these; the flags are gated on the Custom type and
forced false for every other type.
A Custom member holding manage_users + manage_groups (but without collection
access) could add a member -- including themselves via edit_member, or an
attacker-controlled invitee via send_invite -- to an access_all or
collection-bearing group, and thereby indirectly gain read access to those
collections' cipher contents. This bypassed the collection-access boundary
already enforced in put_group_members and add_update_group.
Gate both member-centric group-assignment paths on collection-management
rights via a shared `may_change_group_membership` predicate: callers who
cannot manage collections may only add/remove membership of groups that
confer no collection access, and collection-bearing memberships are left
untouched (neither granted nor revoked). Admins/Owners and full-access
callers keep the previous full-replace behaviour.
Add a regression unit test for the predicate.
Tighten two custom-role read paths that were broader than intended:
- get_group, get_group_details and get_group_members only required
ManagerHeadersLoose, exposing group metadata, collection mappings and
membership to any confirmed Manager/Custom member. Require
ManageGroupsHeaders (Admin/Owner or manage_groups) instead.
- list_policies returned the full policy configuration to any manage_*
member. Keep the endpoint reachable so the Admin Console still loads,
but return an empty list to callers without manage_policies.
Security fixes and hardening following a review of the custom-role
permissions feature:
- Gate the manage_* flags on the Custom membership type via new
Membership::has_manage_* helpers. Previously a stale flag (e.g. left
over after changing a member's type through the admin panel, which
does not go through edit_member) would keep granting management
permissions to a member of any type.
- Clear the manage_* flags in admin::update_membership_type when a
member is changed away from the Custom type.
- edit_member: reject any *change* to the manage_* flags by non-admin
callers. This keeps the previous protection against granting flags,
and additionally prevents a manage_users member from silently
stripping flags an Admin/Owner granted to fellow Custom members.
Unchanged flags still pass, so the regular edit dialog keeps working.
- list_policies: restrict the full policy list to Admins/Owners and
Custom members holding at least one management permission. The
previous loosening to ManagerHeadersLoose also exposed all policies
(including their configuration data) to plain Managers.
- Collection list metadata (get_org_collections{,_details}) is now only
readable with manage_users or manage_groups; manage_policies does not
need the collection list.
- find_confirmed_and_manage_all_by_org: include Custom members with
access_all, matching the pre-existing behaviour for Managers.
- Fix the partial_cmp_MembershipType unit test (Custom is no longer
converted to Manager) and document that Manager and Custom share an
access level in Ord while remaining distinct roles under PartialEq.
- Replace the remaining stale "HACK" comments and the raw "4" type
comparison in send_invite; drop unrelated .gitignore entries.
A Custom member with only manage_users could set the "manage all
collections" child permissions (createNewCollections / editAnyCollection /
deleteAnyCollection) on any member, including themselves, via
POST /organizations/<org_id>/users/<member_id>. edit_member wrote
member_to_edit.access_all unconditionally, so the resulting access_all=true
granted full access to every collection's contents - defeating the
"manage users without collection access" guarantee.
Gate the access_all change on caller_can_manage_collections (Admins/Owners
or full-access members), mirroring how put_group preserves a group's
access_all for callers without collection rights. For everyone else the
member's existing access_all is left untouched. The collection- and
group-assignment paths were already gated; this closes the remaining
direct path.
* Update to Rust 2024 Edition
Updated to the Rust 2024 Edition and added and fixed several lint checks.
This is a large change which, because of the extra lints, added some possible fixes for issues.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Reorder and merge imports
Signed-off-by: BlackDex <black.dex@gmail.com>
* Remove "db_run!" macro calls where possible
Signed-off-by: BlackDex <black.dex@gmail.com>
---------
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update crates and gha
Updated all the crates
Updated GitHub Actions
Signed-off-by: BlackDex <black.dex@gmail.com>
* Fix restoring revoked user
A new endpoint is used to restore a revoked user.
This commit fixes that.
Fixes#7224
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update datatables
Signed-off-by: BlackDex <black.dex@gmail.com>
---------
Signed-off-by: BlackDex <black.dex@gmail.com>
* deps: upgrade the reqwest stack to 0.13
The reqwest 0.13 rustls feature selects the aws-lc provider. Use
rustls-no-provider instead, add rustls 0.23 with the ring provider, and
install that provider at process startup. This keeps Vaultwarden on the
existing ring crypto provider while giving reqwest, OpenDAL and lettre a
process-wide rustls provider.
Disable openidconnect default features and provide a small
AsyncHttpClient wrapper around Vaultwarden's shared reqwest client
builder. This preserves custom DNS, request blocking, timeouts and the
no-redirect OIDC behavior without openidconnect enabling its own reqwest
stack.
Upgrade yubico_ng to 0.15.0 and OpenDAL to 0.56.0. OpenDAL 0.56 also
moves S3 signing to reqsign 3, so switch the optional S3 dependencies
from reqsign/anyhow to reqsign-core and reqsign-aws-v4 and adapt the AWS
SDK credential bridge to the new ProvideCredential API.
Adjust the local OpenDAL call sites for the 0.56 API: use the FS_SCHEME
constant for filesystem checks and replace deprecated remove_all() with
delete_with(...).recursive(true) for Send file cleanup.
* storage: add OpenDAL S3 URI options
OpenDAL S3 storage accepts bucket and root path data today, but
serverless deployments also need URI query parameters to describe provider
behavior in one DATA_FOLDER value.
Update OpenDAL to 0.56.0 and build S3 operators with
S3Config::from_uri(). Keep Vaultwarden's AWS SDK credential chain by
installing a reqsign provider when the URI does not explicitly request
OpenDAL-native credential handling.
Move path handling and operator construction into storage.rs so S3-specific
parsing, credential setup, and URI path manipulation stay out of
configuration handling. Local filesystem behavior is unchanged, and S3
child paths are derived before query strings.
The collection update endpoints (post_collections_update and
post_collections_admin) call .unwrap() on cipher.organization_uuid
in four places. If a user-owned cipher without an organization
somehow reaches these code paths, the server would panic.
Extract the organization UUID early with a descriptive error message
instead of relying on .unwrap(), preventing potential panics and
providing a clear API error response.
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
* Add archiving
* Update Diesel macros and remove unnecessary SUPPORTED_FEATURE_FLAG
* Add IF EXISTS to down.sql migratinos
* Rename migration folders, separate logic based on PR threads
* Ensure SSO token is only usable on the same client
This commit adds an extra check via cookies to ensure the same browser/client is used to request and provide the SSO token.
Previously it would be able to provide a custom link which attackers could use to steal data.
While an attacker would still need the Master Password to be able to decrypt or execute specific actions, they were able to fetch encrypted data.
Solved with some help of Claude Code.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Check email-verified on SSO login/create
This commit prevents possible account takeover via SSO which doesn't check/validate or provide validated status of the email.
It was checked at other locations, but was skipped here.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Prevent data disclosure via SSO endpoints
This commit prevents some data disclosure and user enumeration by only returning the fake SSO identifier.
Since we do not check the identifier anywhere useful, returning the fake one is just fine.
During an invite to an org, that link contains the correct UUID and will be used for the master password requirements.
For anything else, server admins should set the `SSO_MASTER_PASSWORD_POLICY` env variable.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Adjust admin layout to fix issues when SSO is enabled
Signed-off-by: BlackDex <black.dex@gmail.com>
---------
Signed-off-by: BlackDex <black.dex@gmail.com>
Quote from the lint description:
"More flexibility, better memory optimization, and more idiomatic Rust code.
&Option<T> in a function signature breaks encapsulation because the caller must own T and move it into an Option to call with it. When returned, the owner must internally store it as Option<T> in order to return it. At a lower level, &Option<T> points to memory with the presence bit flag plus the T value, whereas Option<&T> is usually optimized to a single pointer, so it may be more optimal."
Quote from lint description:
"Using a smaller unit for a duration that is evenly divisible by a larger unit reduces readability. Readers have to mentally convert values, which can be error-prone and makes the code less clear."
- Updated web-vault to v2026.3.1
Added a new endpoint needed for the admin console to work
- Updated all crates including webpki CVE fixes - Closes#7115
- Updated GHA
Signed-off-by: BlackDex <black.dex@gmail.com>
managers without the access_all flag should not be able to create
collections. the manage all collections permission actually consists of
three separate custom permissions that have not been implemented yet for
more fine-grain access control.
In newer versions of Bitwarden Clients instead of using `null` the folder_id will be an empty string.
This commit adds a special deserialize_with function to keep the same way of working code-wise.
Fixes#6962
Signed-off-by: BlackDex <black.dex@gmail.com>
When a security-stamp gets reset/rotated we should also rotate all device refresh-tokens to invalidate them.
Else clients are still able to use old refresh tokens.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update Feature Flags
Added new feature flags which could be supported without issues.
Removed all deprecated feature flags and only match supported flags.
Do not error on invalid flags during load, but do on config save via admin interface.
During load it will print a `WARNING`, this is to prevent breaking setups when flags are removed, but are still configured.
There are no feature flags anymore currently needed to be set by default, so those are removed now.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Adjust code a bit and add Diagnostics check
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update .env template
Signed-off-by: BlackDex <black.dex@gmail.com>
---------
Signed-off-by: BlackDex <black.dex@gmail.com>
Newer versions of the Bitwarden client use Base64 with padding.
Since this is not a streaming string, but a defined length, we can just strip the `=` chars.
Fixes#6960
Signed-off-by: BlackDex <black.dex@gmail.com>
- Update Rust to v1.93.1
- Updated all the crates
Adjust changes needed for the newer `rand` crate
- Updated GitHub Actions
Signed-off-by: BlackDex <black.dex@gmail.com>
Fix an issue where it was possible for users who were not eligible to access all org ciphers to be able to download and extract the encrypted contents.
Only Managers with full access and Admins and Owners should be able to access this endpoint.
This change will block and prevent access for other users.
Signed-off-by: BlackDex <black.dex@gmail.com>