CVE-2024-55225
Description
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.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Authentication bypass in Vaultwarden prior to v1.32.5 allows attackers to impersonate any user, including administrators, via crafted authorization requests.
Vulnerability
Overview
An authentication bypass vulnerability exists in Vaultwarden versions prior to 1.32.5, specifically in the _password_login function within src/api/identity.rs. The issue arises when the login endpoint handles an auth_request parameter: instead of verifying the user's master password, it checks an access code associated with an authentication request. If an attacker provides a valid auth request UUID and the corresponding access code, the password check is bypassed entirely [1].
Exploitation
An attacker can exploit this by initiating an authentication request (e.g., via the Bitwarden admin approval flow) to obtain a valid auth request UUID. They then craft a login request with that UUID and the associated access code. No knowledge of the target user's password is required. The attacker can target any user, including administrators, as long as they can initiate an authentication request for that user [1].
Impact
Successful exploitation allows the attacker to impersonate the targeted user, gaining full access to their vault and any associated permissions. For administrator accounts, this means complete compromise of the Vaultwarden instance, including access to all stored passwords and secrets [1].
Mitigation
The vulnerability is fixed in Vaultwarden version 1.32.5 and later. Users are strongly advised to upgrade immediately. There are no known workarounds for affected versions [1].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
vaultwardencrates.io | < 1.32.5 | 1.32.5 |
Affected products
3- Vaultwarden/Vaultwardendescription
- Range: <1.32.5
Patches
237c14c3c69b2More authrequest fixes (#5176)
2 files changed · +47 −36
src/api/core/accounts.rs+13 −10 modified@@ -1136,15 +1136,15 @@ async fn post_auth_request( #[get("/auth-requests/<uuid>")] async fn get_auth_request(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult { - if headers.user.uuid != uuid { - err!("AuthRequest doesn't exist", "User uuid's do not match") - } - let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await { Some(auth_request) => auth_request, None => err!("AuthRequest doesn't exist", "Record not found"), }; + if headers.user.uuid != auth_request.user_uuid { + err!("AuthRequest doesn't exist", "User uuid's do not match") + } + let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date)); Ok(Json(json!({ @@ -1190,15 +1190,18 @@ async fn put_auth_request( err!("AuthRequest doesn't exist", "User uuid's do not match") } - auth_request.approved = Some(data.request_approved); - auth_request.enc_key = Some(data.key); - auth_request.master_password_hash = data.master_password_hash; - auth_request.response_device_id = Some(data.device_identifier.clone()); - auth_request.save(&mut conn).await?; + if data.request_approved { + auth_request.approved = Some(data.request_approved); + auth_request.enc_key = Some(data.key); + auth_request.master_password_hash = data.master_password_hash; + auth_request.response_device_id = Some(data.device_identifier.clone()); + auth_request.save(&mut conn).await?; - if auth_request.approved.unwrap_or(false) { ant.send_auth_response(&auth_request.user_uuid, &auth_request.uuid).await; nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, data.device_identifier, &mut conn).await; + } else { + // If denied, there's no reason to keep the request + auth_request.delete(&mut conn).await?; } let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date));
src/api/identity.rs+34 −26 modified@@ -165,27 +165,46 @@ async fn _password_login( // Set the user_uuid here to be passed back used for event logging. *user_uuid = Some(user.uuid.clone()); - // Check password - let password = data.password.as_ref().unwrap(); - if let Some(auth_request_uuid) = data.auth_request.clone() { - if let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_uuid.as_str(), conn).await { - if !auth_request.check_access_code(password) { - err!( - "Username or access code is incorrect. Try again", - format!("IP: {}. Username: {}.", ip.ip, username), - ErrorEvent { - event: EventType::UserFailedLogIn, - } - ) + // Check if the user is disabled + if !user.enabled { + err!( + "This user has been disabled", + format!("IP: {}. Username: {}.", ip.ip, username), + ErrorEvent { + event: EventType::UserFailedLogIn } - } else { + ) + } + + let password = data.password.as_ref().unwrap(); + + // If we get an auth request, we don't check the user's password, but the access code of the auth request + if let Some(ref auth_request_uuid) = data.auth_request { + let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_uuid.as_str(), conn).await else { err!( "Auth request not found. Try again.", format!("IP: {}. Username: {}.", ip.ip, username), ErrorEvent { event: EventType::UserFailedLogIn, } ) + }; + + // Delete the request after we used it + auth_request.delete(conn).await?; + + if auth_request.user_uuid != user.uuid + || !auth_request.approved.unwrap_or(false) + || ip.ip.to_string() != auth_request.request_ip + || !auth_request.check_access_code(password) + { + err!( + "Username or access code is incorrect. Try again", + format!("IP: {}. Username: {}.", ip.ip, username), + ErrorEvent { + event: EventType::UserFailedLogIn, + } + ) } } else if !user.check_valid_password(password) { err!( @@ -197,8 +216,8 @@ async fn _password_login( ) } - // Change the KDF Iterations - if user.password_iterations != CONFIG.password_iterations() { + // Change the KDF Iterations (only when not logging in with an auth request) + if data.auth_request.is_none() && user.password_iterations != CONFIG.password_iterations() { user.password_iterations = CONFIG.password_iterations(); user.set_password(password, None, false, None); @@ -207,17 +226,6 @@ async fn _password_login( } } - // Check if the user is disabled - if !user.enabled { - err!( - "This user has been disabled", - format!("IP: {}. Username: {}.", ip.ip, username), - ErrorEvent { - event: EventType::UserFailedLogIn - } - ) - } - let now = Utc::now().naive_utc(); if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() {
20d9e885bfcdUpdate crates and fix several issues
9 files changed · +279 −198
Cargo.lock+109 −78 modified@@ -55,9 +55,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "611cc2ae7d2e242c457e4be7f97036b8ad9ca152b499f53faf99b1ed8fc2553f" [[package]] name = "android-tzdata" @@ -153,9 +153,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ "async-lock", "cfg-if", @@ -352,9 +352,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bigdecimal" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d712318a27c7150326677b321a5fa91b55f6d9034ffd67f20319e147d40cee" +checksum = "8f850665a0385e070b64c38d2354e6c104c8479c59868d1e48a0c13ee2c7a1c1" dependencies = [ "autocfg", "libm", @@ -459,9 +459,9 @@ checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "cached" -version = "0.53.1" +version = "0.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4d73155ae6b28cf5de4cfc29aeb02b8a1c6dab883cb015d15cd514e42766846" +checksum = "9718806c4a2fe9e8a56fd736f97b340dd10ed1be8ed733ed50449f351dc33cae" dependencies = [ "ahash", "async-trait", @@ -495,9 +495,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" [[package]] name = "cc" -version = "1.1.31" +version = "1.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf" dependencies = [ "shlex", ] @@ -574,12 +574,13 @@ dependencies = [ [[package]] name = "cookie_store" -version = "0.21.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" dependencies = [ "cookie", - "idna 0.5.0", + "document-features", + "idna 1.0.3", "log", "publicsuffix", "serde", @@ -842,6 +843,15 @@ dependencies = [ "syn", ] +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -962,9 +972,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" [[package]] name = "fern" @@ -1082,9 +1092,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" dependencies = [ "fastrand", "futures-core", @@ -1267,11 +1277,12 @@ checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" [[package]] name = "handlebars" -version = "6.1.0" +version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25b617d1375ef96eeb920ae717e3da34a02fc979fe632c75128350f9e1f74a" +checksum = "fd4ccde012831f9a071a637b0d4e31df31c0f6c525784b35ae76a9ac6bc1e315" dependencies = [ "log", + "num-order", "pest", "pest_derive", "serde", @@ -1292,9 +1303,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" [[package]] name = "heck" @@ -1401,9 +1412,9 @@ dependencies = [ [[package]] name = "html5gum" -version = "0.5.7" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4e556171a058ba117bbe88b059fb37b6289023e007d2903ea6dca3a3cbff14" +checksum = "91b361633dcc40096d01de35ed535b6089be91880be47b6fd8f560497af7f716" dependencies = [ "jetscii", ] @@ -1530,7 +1541,7 @@ dependencies = [ "http 1.1.0", "hyper 1.5.0", "hyper-util", - "rustls 0.23.15", + "rustls 0.23.16", "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", @@ -1568,9 +1579,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", "futures-channel", @@ -1754,24 +1765,23 @@ dependencies = [ [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", ] [[package]] -name = "idna" -version = "1.0.2" +name = "idna_adapter" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd69211b9b519e98303c015e21a007e293db403b6c85b9b124e133d25e242cdd" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ "icu_normalizer", "icu_properties", - "smallvec", - "utf8_iter", ] [[package]] @@ -1781,7 +1791,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown 0.15.1", "serde", ] @@ -1899,7 +1909,7 @@ dependencies = [ "futures-util", "hostname 0.4.0", "httpdate", - "idna 1.0.2", + "idna 1.0.3", "mime", "native-tls", "nom", @@ -1915,15 +1925,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.161" +version = "0.2.162" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libmimalloc-sys" @@ -1964,6 +1974,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + [[package]] name = "lock_api" version = "0.4.12" @@ -2205,6 +2221,21 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-modular" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" + +[[package]] +name = "num-order" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" +dependencies = [ + "num-modular", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2493,9 +2524,9 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -2522,9 +2553,9 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "polling" -version = "3.7.3" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", @@ -2729,9 +2760,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -2824,9 +2855,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.8" +version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" dependencies = [ "async-compression", "base64 0.22.1", @@ -3041,9 +3072,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" dependencies = [ "bitflags 2.6.0", "errno", @@ -3066,9 +3097,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.15" +version = "0.23.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993" +checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" dependencies = [ "once_cell", "rustls-pki-types", @@ -3198,9 +3229,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" dependencies = [ "core-foundation-sys", "libc", @@ -3214,9 +3245,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.213" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] @@ -3233,9 +3264,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.213" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", @@ -3440,9 +3471,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.85" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -3532,9 +3563,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", @@ -3545,18 +3576,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.65" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.65" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", @@ -3642,9 +3673,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.41.0" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", "bytes", @@ -3695,7 +3726,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.15", + "rustls 0.23.16", "rustls-pki-types", "tokio", ] @@ -3953,12 +3984,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" dependencies = [ "form_urlencoded", - "idna 0.5.0", + "idna 1.0.3", "percent-encoding", "serde", ] @@ -4043,7 +4074,7 @@ dependencies = [ "pico-args", "rand", "regex", - "reqwest 0.12.8", + "reqwest 0.12.9", "ring", "rmpv", "rocket", @@ -4170,9 +4201,9 @@ checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "wasm-streams" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" dependencies = [ "futures-util", "js-sys", @@ -4222,9 +4253,9 @@ dependencies = [ [[package]] name = "which" -version = "6.0.3" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +checksum = "c9cad3279ade7346b96e38731a641d7343dd6a53d55083dd54eadfa5a1b38c6b" dependencies = [ "either", "home",
Cargo.toml+11 −11 modified@@ -53,7 +53,7 @@ once_cell = "1.20.2" # Numerical libraries num-traits = "0.2.19" num-derive = "0.4.2" -bigdecimal = "0.4.5" +bigdecimal = "0.4.6" # Web framework rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false } @@ -67,10 +67,10 @@ dashmap = "6.1.0" # Async futures futures = "0.3.31" -tokio = { version = "1.41.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } +tokio = { version = "1.41.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } # A generic serialization/deserialization framework -serde = { version = "1.0.213", features = ["derive"] } +serde = { version = "1.0.214", features = ["derive"] } serde_json = "1.0.132" # A safe, extensible ORM and Query builder @@ -112,32 +112,32 @@ yubico = { version = "0.11.0", features = ["online-tokio"], default-features = f webauthn-rs = "0.3.2" # Handling of URL's for WebAuthn and favicons -url = "2.5.2" +url = "2.5.3" # Email libraries lettre = { version = "0.11.10", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false } percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails email_address = "0.2.9" # HTML Template library -handlebars = { version = "6.1.0", features = ["dir_source"] } +handlebars = { version = "6.2.0", features = ["dir_source"] } # HTTP client (Used for favicons, version check, DUO and HIBP API) -reqwest = { version = "0.12.8", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] } +reqwest = { version = "0.12.9", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] } hickory-resolver = "0.24.1" # Favicon extraction libraries -html5gum = "0.5.7" -regex = { version = "1.11.0", features = ["std", "perf", "unicode-perl"], default-features = false } +html5gum = "0.6.1" +regex = { version = "1.11.1", features = ["std", "perf", "unicode-perl"], default-features = false } data-url = "0.3.1" bytes = "1.8.0" # Cache function results (Used for version check and favicon fetching) -cached = { version = "0.53.1", features = ["async"] } +cached = { version = "0.54.0", features = ["async"] } # Used for custom short lived cookie jar during favicon extraction cookie = "0.18.1" -cookie_store = "0.21.0" +cookie_store = "0.21.1" # Used by U2F, JWT and PostgreSQL openssl = "0.10.68" @@ -155,7 +155,7 @@ semver = "1.0.23" # Allow overriding the default memory allocator # Mainly used for the musl builds, since the default musl malloc is very slow mimalloc = { version = "0.1.43", features = ["secure"], default-features = false, optional = true } -which = "6.0.3" +which = "7.0.0" # Argon2 library with support for the PHC format argon2 = "0.5.3"
src/api/core/accounts.rs+89 −87 modified@@ -1,5 +1,5 @@ use crate::db::DbPool; -use chrono::{SecondsFormat, Utc}; +use chrono::Utc; use rocket::serde::json::Json; use serde_json::Value; @@ -13,7 +13,7 @@ use crate::{ crypto, db::{models::*, DbConn}, mail, - util::NumberOrString, + util::{format_date, NumberOrString}, CONFIG, }; @@ -901,14 +901,12 @@ pub async fn _prelogin(data: Json<PreloginData>, mut conn: DbConn) -> Json<Value None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT, None, None), }; - let result = json!({ + Json(json!({ "kdf": kdf_type, "kdfIterations": kdf_iter, "kdfMemory": kdf_mem, "kdfParallelism": kdf_para, - }); - - Json(result) + })) } // https://github.com/bitwarden/server/blob/master/src/Api/Models/Request/Accounts/SecretVerificationRequestModel.cs @@ -1084,31 +1082,36 @@ struct AuthRequestRequest { device_identifier: String, email: String, public_key: String, - #[serde(alias = "type")] - _type: i32, + // Not used for now + // #[serde(alias = "type")] + // _type: i32, } #[post("/auth-requests", data = "<data>")] async fn post_auth_request( data: Json<AuthRequestRequest>, - headers: ClientHeaders, + client_headers: ClientHeaders, mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { let data = data.into_inner(); let user = match User::find_by_mail(&data.email, &mut conn).await { Some(user) => user, - None => { - err!("AuthRequest doesn't exist") - } + None => err!("AuthRequest doesn't exist", "User not found"), }; + // Validate device uuid and type + match Device::find_by_uuid_and_user(&data.device_identifier, &user.uuid, &mut conn).await { + Some(device) if device.atype == client_headers.device_type => {} + _ => err!("AuthRequest doesn't exist", "Device verification failed"), + } + let mut auth_request = AuthRequest::new( user.uuid.clone(), data.device_identifier.clone(), - headers.device_type, - headers.ip.ip.to_string(), + client_headers.device_type, + client_headers.ip.ip.to_string(), data.access_code, data.public_key, ); @@ -1123,7 +1126,7 @@ async fn post_auth_request( "requestIpAddress": auth_request.request_ip, "key": null, "masterPasswordHash": null, - "creationDate": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true), + "creationDate": format_date(&auth_request.creation_date), "responseDate": null, "requestApproved": false, "origin": CONFIG.domain_origin(), @@ -1132,33 +1135,31 @@ async fn post_auth_request( } #[get("/auth-requests/<uuid>")] -async fn get_auth_request(uuid: &str, mut conn: DbConn) -> JsonResult { +async fn get_auth_request(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult { + if headers.user.uuid != uuid { + err!("AuthRequest doesn't exist", "User uuid's do not match") + } + let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await { Some(auth_request) => auth_request, - None => { - err!("AuthRequest doesn't exist") - } + None => err!("AuthRequest doesn't exist", "Record not found"), }; - let response_date_utc = auth_request - .response_date - .map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true)); - - Ok(Json(json!( - { - "id": uuid, - "publicKey": auth_request.public_key, - "requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(), - "requestIpAddress": auth_request.request_ip, - "key": auth_request.enc_key, - "masterPasswordHash": auth_request.master_password_hash, - "creationDate": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true), - "responseDate": response_date_utc, - "requestApproved": auth_request.approved, - "origin": CONFIG.domain_origin(), - "object":"auth-request" - } - ))) + let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date)); + + Ok(Json(json!({ + "id": uuid, + "publicKey": auth_request.public_key, + "requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(), + "requestIpAddress": auth_request.request_ip, + "key": auth_request.enc_key, + "masterPasswordHash": auth_request.master_password_hash, + "creationDate": format_date(&auth_request.creation_date), + "responseDate": response_date_utc, + "requestApproved": auth_request.approved, + "origin": CONFIG.domain_origin(), + "object":"auth-request" + }))) } #[derive(Debug, Deserialize)] @@ -1174,18 +1175,21 @@ struct AuthResponseRequest { async fn put_auth_request( uuid: &str, data: Json<AuthResponseRequest>, + headers: Headers, mut conn: DbConn, ant: AnonymousNotify<'_>, nt: Notify<'_>, ) -> JsonResult { let data = data.into_inner(); let mut auth_request: AuthRequest = match AuthRequest::find_by_uuid(uuid, &mut conn).await { Some(auth_request) => auth_request, - None => { - err!("AuthRequest doesn't exist") - } + None => err!("AuthRequest doesn't exist", "Record not found"), }; + if headers.user.uuid != auth_request.user_uuid { + err!("AuthRequest doesn't exist", "User uuid's do not match") + } + auth_request.approved = Some(data.request_approved); auth_request.enc_key = Some(data.key); auth_request.master_password_hash = data.master_password_hash; @@ -1197,59 +1201,57 @@ async fn put_auth_request( nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, data.device_identifier, &mut conn).await; } - let response_date_utc = auth_request - .response_date - .map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true)); - - Ok(Json(json!( - { - "id": uuid, - "publicKey": auth_request.public_key, - "requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(), - "requestIpAddress": auth_request.request_ip, - "key": auth_request.enc_key, - "masterPasswordHash": auth_request.master_password_hash, - "creationDate": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true), - "responseDate": response_date_utc, - "requestApproved": auth_request.approved, - "origin": CONFIG.domain_origin(), - "object":"auth-request" - } - ))) + let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date)); + + Ok(Json(json!({ + "id": uuid, + "publicKey": auth_request.public_key, + "requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(), + "requestIpAddress": auth_request.request_ip, + "key": auth_request.enc_key, + "masterPasswordHash": auth_request.master_password_hash, + "creationDate": format_date(&auth_request.creation_date), + "responseDate": response_date_utc, + "requestApproved": auth_request.approved, + "origin": CONFIG.domain_origin(), + "object":"auth-request" + }))) } #[get("/auth-requests/<uuid>/response?<code>")] -async fn get_auth_request_response(uuid: &str, code: &str, mut conn: DbConn) -> JsonResult { +async fn get_auth_request_response( + uuid: &str, + code: &str, + client_headers: ClientHeaders, + mut conn: DbConn, +) -> JsonResult { let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await { Some(auth_request) => auth_request, - None => { - err!("AuthRequest doesn't exist") - } + None => err!("AuthRequest doesn't exist", "User not found"), }; - if !auth_request.check_access_code(code) { - err!("Access code invalid doesn't exist") + if auth_request.device_type != client_headers.device_type + && auth_request.request_ip != client_headers.ip.ip.to_string() + && !auth_request.check_access_code(code) + { + err!("AuthRequest doesn't exist", "Invalid device, IP or code") } - let response_date_utc = auth_request - .response_date - .map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true)); - - Ok(Json(json!( - { - "id": uuid, - "publicKey": auth_request.public_key, - "requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(), - "requestIpAddress": auth_request.request_ip, - "key": auth_request.enc_key, - "masterPasswordHash": auth_request.master_password_hash, - "creationDate": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true), - "responseDate": response_date_utc, - "requestApproved": auth_request.approved, - "origin": CONFIG.domain_origin(), - "object":"auth-request" - } - ))) + let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date)); + + Ok(Json(json!({ + "id": uuid, + "publicKey": auth_request.public_key, + "requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(), + "requestIpAddress": auth_request.request_ip, + "key": auth_request.enc_key, + "masterPasswordHash": auth_request.master_password_hash, + "creationDate": format_date(&auth_request.creation_date), + "responseDate": response_date_utc, + "requestApproved": auth_request.approved, + "origin": CONFIG.domain_origin(), + "object":"auth-request" + }))) } #[get("/auth-requests")] @@ -1261,7 +1263,7 @@ async fn get_auth_requests(headers: Headers, mut conn: DbConn) -> JsonResult { .iter() .filter(|request| request.approved.is_none()) .map(|request| { - let response_date_utc = request.response_date.map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true)); + let response_date_utc = request.response_date.map(|response_date| format_date(&response_date)); json!({ "id": request.uuid, @@ -1270,7 +1272,7 @@ async fn get_auth_requests(headers: Headers, mut conn: DbConn) -> JsonResult { "requestIpAddress": request.request_ip, "key": request.enc_key, "masterPasswordHash": request.master_password_hash, - "creationDate": request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true), + "creationDate": format_date(&request.creation_date), "responseDate": response_date_utc, "requestApproved": request.approved, "origin": CONFIG.domain_origin(),
src/api/core/mod.rs+1 −0 modified@@ -136,6 +136,7 @@ async fn put_eq_domains(data: Json<EquivDomainData>, headers: Headers, conn: DbC #[get("/hibp/breach?<username>")] async fn hibp_breach(username: &str) -> JsonResult { + let username: String = url::form_urlencoded::byte_serialize(username.as_bytes()).collect(); let url = format!( "https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false" );
src/db/models/cipher.rs+7 −5 modified@@ -1,6 +1,6 @@ use crate::util::LowerCase; use crate::CONFIG; -use chrono::{DateTime, NaiveDateTime, TimeDelta, Utc}; +use chrono::{NaiveDateTime, TimeDelta, Utc}; use serde_json::Value; use super::{ @@ -216,11 +216,13 @@ impl Cipher { Some(p) if p.is_string() => Some(d.data), _ => None, }) - .map(|d| match d.get("lastUsedDate").and_then(|l| l.as_str()) { - Some(l) if DateTime::parse_from_rfc3339(l).is_ok() => d, + .map(|mut d| match d.get("lastUsedDate").and_then(|l| l.as_str()) { + Some(l) => { + d["lastUsedDate"] = json!(crate::util::validate_and_format_date(l)); + d + } _ => { - let mut d = d; - d["lastUsedDate"] = json!("1970-01-01T00:00:00.000Z"); + d["lastUsedDate"] = json!("1970-01-01T00:00:00.000000Z"); d } })
src/mail.rs+48 −9 modified@@ -96,7 +96,31 @@ fn smtp_transport() -> AsyncSmtpTransport<Tokio1Executor> { smtp_client.build() } +// This will sanitize the string values by stripping all the html tags to prevent XSS and HTML Injections +fn sanitize_data(data: &mut serde_json::Value) { + use regex::Regex; + use std::sync::LazyLock; + static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"<[^>]+>").unwrap()); + + match data { + serde_json::Value::String(s) => *s = RE.replace_all(s, "").to_string(), + serde_json::Value::Object(obj) => { + for d in obj.values_mut() { + sanitize_data(d); + } + } + serde_json::Value::Array(arr) => { + for d in arr.iter_mut() { + sanitize_data(d); + } + } + _ => {} + } +} + fn get_text(template_name: &'static str, data: serde_json::Value) -> Result<(String, String, String), Error> { + let mut data = data; + sanitize_data(&mut data); let (subject_html, body_html) = get_template(&format!("{template_name}.html"), &data)?; let (_subject_text, body_text) = get_template(template_name, &data)?; Ok((subject_html, body_html, body_text)) @@ -116,6 +140,10 @@ fn get_template(template_name: &str, data: &serde_json::Value) -> Result<(String None => err!("Template doesn't contain body"), }; + if text_split.next().is_some() { + err!("Template contains more than one body"); + } + Ok((subject, body)) } @@ -259,16 +287,15 @@ pub async fn send_invite( } let query_string = match query.query() { - None => err!(format!("Failed to build invite URL query parameters")), + None => err!("Failed to build invite URL query parameters"), Some(query) => query, }; - // `url.Url` would place the anchor `#` after the query parameters - let url = format!("{}/#/accept-organization/?{}", CONFIG.domain(), query_string); let (subject, body_html, body_text) = get_text( "email/send_org_invite", json!({ - "url": url, + // `url.Url` would place the anchor `#` after the query parameters + "url": format!("{}/#/accept-organization/?{}", CONFIG.domain(), query_string), "img_src": CONFIG._smtp_img_src(), "org_name": org_name, }), @@ -292,17 +319,29 @@ pub async fn send_emergency_access_invite( String::from(grantor_email), ); - let invite_token = encode_jwt(&claims); + // Build the query here to ensure proper escaping + let mut query = url::Url::parse("https://query.builder").unwrap(); + { + let mut query_params = query.query_pairs_mut(); + query_params + .append_pair("id", emer_id) + .append_pair("name", grantor_name) + .append_pair("email", address) + .append_pair("token", &encode_jwt(&claims)); + } + + let query_string = match query.query() { + None => err!("Failed to build emergency invite URL query parameters"), + Some(query) => query, + }; let (subject, body_html, body_text) = get_text( "email/send_emergency_access_invite", json!({ - "url": CONFIG.domain(), + // `url.Url` would place the anchor `#` after the query parameters + "url": format!("{}/#/accept-emergency/?{query_string}", CONFIG.domain()), "img_src": CONFIG._smtp_img_src(), - "emer_id": emer_id, - "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), "grantor_name": grantor_name, - "token": invite_token, }), )?;
src/static/templates/email/send_emergency_access_invite.hbs+1 −1 modified@@ -2,7 +2,7 @@ Emergency access for {{{grantor_name}}} <!----------------> You have been invited to become an emergency contact for {{grantor_name}}. To accept this invite, click the following link: -Click here to join: {{url}}/#/accept-emergency/?id={{emer_id}}&name={{grantor_name}}&email={{email}}&token={{token}} +Click here to join: {{{url}}} If you do not wish to become an emergency contact for {{grantor_name}}, you can safely ignore this email. {{> email/email_footer_text }}
src/static/templates/email/send_emergency_access_invite.html.hbs+2 −2 modified@@ -9,7 +9,7 @@ Emergency access for {{{grantor_name}}} </tr> <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center"> - <a href="{{url}}/#/accept-emergency/?id={{emer_id}}&name={{grantor_name}}&email={{email}}&token={{token}}" + <a href="{{{url}}}" clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> Become emergency contact </a> @@ -21,4 +21,4 @@ Emergency access for {{{grantor_name}}} </td> </tr> </table> -{{> email/email_footer }} \ No newline at end of file +{{> email/email_footer }}
src/util.rs+11 −5 modified@@ -438,13 +438,19 @@ pub fn get_env_bool(key: &str) -> Option<bool> { use chrono::{DateTime, Local, NaiveDateTime, TimeZone}; -// Format used by Bitwarden API -const DATETIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.6fZ"; - /// Formats a UTC-offset `NaiveDateTime` in the format used by Bitwarden API /// responses with "date" fields (`CreationDate`, `RevisionDate`, etc.). pub fn format_date(dt: &NaiveDateTime) -> String { - dt.format(DATETIME_FORMAT).to_string() + dt.and_utc().to_rfc3339_opts(chrono::SecondsFormat::Micros, true) +} + +/// Validates and formats a RFC3339 timestamp +/// If parsing fails it will return the start of the unix datetime +pub fn validate_and_format_date(dt: &str) -> String { + match DateTime::parse_from_rfc3339(dt) { + Ok(dt) => dt.to_rfc3339_opts(chrono::SecondsFormat::Micros, true), + _ => String::from("1970-01-01T00:00:00.000000Z"), + } } /// Formats a `DateTime<Local>` using the specified format string. @@ -486,7 +492,7 @@ pub fn format_datetime_http(dt: &DateTime<Local>) -> String { } pub fn parse_date(date: &str) -> NaiveDateTime { - NaiveDateTime::parse_from_str(date, DATETIME_FORMAT).unwrap() + DateTime::parse_from_rfc3339(date).unwrap().naive_utc() } //
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-x7m9-mv49-fv73ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-55225ghsaADVISORY
- github.com/dani-garcia/vaultwarden/commit/20d9e885bfcd7df7828d92c6e59ed5fe7b40a879ghsaWEB
- github.com/dani-garcia/vaultwarden/commit/37c14c3c69b244ec50f5c62b4c9260171607c1d8ghsaWEB
- github.com/dani-garcia/vaultwarden/releases/tag/1.32.4ghsaWEB
- github.com/dani-garcia/vaultwarden/releases/tag/1.32.5ghsaWEB
- insinuator.net/2024/11/vulnerability-disclosure-authentication-bypass-in-vaultwarden-versions-1-32-5ghsaWEB
- insinuator.net/2024/11/vulnerability-disclosure-authentication-bypass-in-vaultwarden-versions-1-32-5/mitre
News mentions
0No linked articles in our index yet.