summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBlackDex <[email protected]>2024-11-09 19:58:10 +0100
committerDaniel GarcĂ­a <[email protected]>2024-11-10 23:56:19 +0100
commit20d9e885bfcd7df7828d92c6e59ed5fe7b40a879 (patch)
tree74d2fd00ad1ea74e5de7e98049c724d0b6540c4b /src
parent2f20ad86f9283419845ff1b77c504c3dfd043578 (diff)
downloadvaultwarden-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.rs176
-rw-r--r--src/api/core/mod.rs1
-rw-r--r--src/db/models/cipher.rs12
-rw-r--r--src/mail.rs57
-rw-r--r--src/static/templates/email/send_emergency_access_invite.hbs2
-rw-r--r--src/static/templates/email/send_emergency_access_invite.html.hbs4
-rw-r--r--src/util.rs16
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()
}
//