diff options
author | BlackDex <[email protected]> | 2024-11-09 19:58:10 +0100 |
---|---|---|
committer | Daniel GarcĂa <[email protected]> | 2024-11-10 23:56:19 +0100 |
commit | 20d9e885bfcd7df7828d92c6e59ed5fe7b40a879 (patch) | |
tree | 74d2fd00ad1ea74e5de7e98049c724d0b6540c4b /src | |
parent | 2f20ad86f9283419845ff1b77c504c3dfd043578 (diff) | |
download | vaultwarden-20d9e885bfcd7df7828d92c6e59ed5fe7b40a879.tar.gz vaultwarden-20d9e885bfcd7df7828d92c6e59ed5fe7b40a879.zip |
Update crates and fix several issues
Signed-off-by: BlackDex <[email protected]>
Diffstat (limited to 'src')
-rw-r--r-- | src/api/core/accounts.rs | 176 | ||||
-rw-r--r-- | src/api/core/mod.rs | 1 | ||||
-rw-r--r-- | src/db/models/cipher.rs | 12 | ||||
-rw-r--r-- | src/mail.rs | 57 | ||||
-rw-r--r-- | src/static/templates/email/send_emergency_access_invite.hbs | 2 | ||||
-rw-r--r-- | src/static/templates/email/send_emergency_access_invite.html.hbs | 4 | ||||
-rw-r--r-- | src/util.rs | 16 |
7 files changed, 159 insertions, 109 deletions
diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index f9822629..71609b37 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -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,14 +1082,15 @@ 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 { @@ -1099,16 +1098,20 @@ async fn post_auth_request( 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,6 +1175,7 @@ struct AuthResponseRequest { async fn put_auth_request( uuid: &str, data: Json<AuthResponseRequest>, + headers: Headers, mut conn: DbConn, ant: AnonymousNotify<'_>, nt: Notify<'_>, @@ -1181,11 +1183,13 @@ async fn put_auth_request( 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(), diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index ee5db190..4ac6b777 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -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" ); diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index fb2b5021..9c568284 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -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 } }) diff --git a/src/mail.rs b/src/mail.rs index b33efd95..5ce4a079 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -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, }), )?; diff --git a/src/static/templates/email/send_emergency_access_invite.hbs b/src/static/templates/email/send_emergency_access_invite.hbs index 9dc114c0..f4817628 100644 --- a/src/static/templates/email/send_emergency_access_invite.hbs +++ b/src/static/templates/email/send_emergency_access_invite.hbs @@ -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 }} diff --git a/src/static/templates/email/send_emergency_access_invite.html.hbs b/src/static/templates/email/send_emergency_access_invite.html.hbs index fd1c0400..5318378c 100644 --- a/src/static/templates/email/send_emergency_access_invite.html.hbs +++ b/src/static/templates/email/send_emergency_access_invite.html.hbs @@ -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 }} diff --git a/src/util.rs b/src/util.rs index d8433b9a..1f3ba9cf 100644 --- a/src/util.rs +++ b/src/util.rs @@ -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() } // |