vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

commit 60964c07e6d178ae632d821e5698eee681f3d3c7
parent e7f083dee9743bfe4937f5c8149fa9d8383edb96
Author: Daniel GarcĂ­a <dani-garcia@users.noreply.github.com>
Date:   Mon,  3 Jul 2023 19:58:14 +0200

Add some extra access checks for attachments and groups

Diffstat:
Msrc/api/core/ciphers.rs | 9+++++++++
Msrc/api/core/organizations.rs | 24++++++++++++++++++------
Msrc/api/core/sends.rs | 37++++++++++++++++++++-----------------
Msrc/api/web.rs | 10++++++++--
Msrc/auth.rs | 30++++++++++++++++++++++++++++++
Msrc/db/models/attachment.rs | 4+++-
Msrc/util.rs | 13++++++++++++-
7 files changed, 100 insertions(+), 27 deletions(-)

diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs @@ -934,6 +934,15 @@ async fn share_cipher_by_uuid( /// redirects to the same location as before the v2 API. #[get("/ciphers/<uuid>/attachment/<attachment_id>")] async fn get_attachment(uuid: &str, attachment_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { + let cipher = match Cipher::find_by_uuid(uuid, &mut conn).await { + Some(cipher) => cipher, + None => err!("Cipher doesn't exist"), + }; + + if !cipher.is_accessible_to_user(&headers.user.uuid, &mut conn).await { + err!("Cipher is not accessible") + } + match Attachment::find_by_id(attachment_id, &mut conn).await { Some(attachment) if uuid == attachment.cipher_uuid => Ok(Json(attachment.to_json(&headers.host))), Some(_) => err!("Attachment doesn't belong to cipher"), diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs @@ -2578,11 +2578,15 @@ async fn put_user_groups( err!("Group support is disabled"); } - match UserOrganization::find_by_uuid(org_user_id, &mut conn).await { - Some(_) => { /* Do nothing */ } + let user_org = match UserOrganization::find_by_uuid(org_user_id, &mut conn).await { + Some(uo) => uo, _ => err!("User could not be found!"), }; + if user_org.org_uuid != org_id { + err!("Group doesn't belong to organization"); + } + GroupUser::delete_all_by_user(org_user_id, &mut conn).await?; let assigned_group_ids = data.into_inner().data; @@ -2628,16 +2632,24 @@ async fn delete_group_user( err!("Group support is disabled"); } - match UserOrganization::find_by_uuid(org_user_id, &mut conn).await { - Some(_) => { /* Do nothing */ } + let user_org = match UserOrganization::find_by_uuid(org_user_id, &mut conn).await { + Some(uo) => uo, _ => err!("User could not be found!"), }; - match Group::find_by_uuid(group_id, &mut conn).await { - Some(_) => { /* Do nothing */ } + if user_org.org_uuid != org_id { + err!("User doesn't belong to organization"); + } + + let group = match Group::find_by_uuid(group_id, &mut conn).await { + Some(g) => g, _ => err!("Group could not be found!"), }; + if group.organizations_uuid != org_id { + err!("Group doesn't belong to organization"); + } + log_event( EventType::OrganizationUserUpdatedGroups as i32, org_user_id, diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs @@ -340,27 +340,30 @@ async fn post_send_file_v2_data( let mut data = data.into_inner(); - if let Some(send) = Send::find_by_uuid(send_uuid, &mut conn).await { - let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(send_uuid); - let file_path = folder_path.join(file_id); - tokio::fs::create_dir_all(&folder_path).await?; + let Some(send) = Send::find_by_uuid(send_uuid, &mut conn).await else { err!("Send not found. Unable to save the file.") }; - if let Err(_err) = data.data.persist_to(&file_path).await { - data.data.move_copy_to(file_path).await? - } + let Some(send_user_id) = &send.user_uuid else {err!("Sends are only supported for users at the moment")}; + if send_user_id != &headers.user.uuid { + err!("Send doesn't belong to user"); + } - nt.send_send_update( - UpdateType::SyncSendCreate, - &send, - &send.update_users_revision(&mut conn).await, - &headers.device.uuid, - &mut conn, - ) - .await; - } else { - err!("Send not found. Unable to save the file."); + let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(send_uuid); + let file_path = folder_path.join(file_id); + tokio::fs::create_dir_all(&folder_path).await?; + + if let Err(_err) = data.data.persist_to(&file_path).await { + data.data.move_copy_to(file_path).await? } + nt.send_send_update( + UpdateType::SyncSendCreate, + &send, + &send.update_users_revision(&mut conn).await, + &headers.device.uuid, + &mut conn, + ) + .await; + Ok(()) } diff --git a/src/api/web.rs b/src/api/web.rs @@ -5,6 +5,7 @@ use serde_json::Value; use crate::{ api::{core::now, ApiResult, EmptyResult}, + auth::decode_file_download, error::Error, util::{Cached, SafeString}, CONFIG, @@ -91,8 +92,13 @@ async fn web_files(p: PathBuf) -> Cached<Option<NamedFile>> { Cached::long(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join(p)).await.ok(), true) } -#[get("/attachments/<uuid>/<file_id>")] -async fn attachments(uuid: SafeString, file_id: SafeString) -> Option<NamedFile> { +#[get("/attachments/<uuid>/<file_id>?<token>")] +async fn attachments(uuid: SafeString, file_id: SafeString, token: String) -> Option<NamedFile> { + let Ok(claims) = dbg!(decode_file_download(&token)) else { return None }; + if claims.sub != *uuid || claims.file_id != *file_id { + return None; + } + NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid).join(file_id)).await.ok() } diff --git a/src/auth.rs b/src/auth.rs @@ -24,6 +24,7 @@ static JWT_VERIFYEMAIL_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|verifyema static JWT_ADMIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|admin", CONFIG.domain_origin())); static JWT_SEND_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|send", CONFIG.domain_origin())); static JWT_ORG_API_KEY_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|api.organization", CONFIG.domain_origin())); +static JWT_FILE_DOWNLOAD_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|file_download", CONFIG.domain_origin())); static PRIVATE_RSA_KEY: Lazy<EncodingKey> = Lazy::new(|| { let key = @@ -98,6 +99,10 @@ pub fn decode_api_org(token: &str) -> Result<OrgApiKeyLoginJwtClaims, Error> { decode_jwt(token, JWT_ORG_API_KEY_ISSUER.to_string()) } +pub fn decode_file_download(token: &str) -> Result<FileDownloadClaims, Error> { + decode_jwt(token, JWT_FILE_DOWNLOAD_ISSUER.to_string()) +} + #[derive(Debug, Serialize, Deserialize)] pub struct LoginJwtClaims { // Not before @@ -235,6 +240,31 @@ pub fn generate_organization_api_key_login_claims(uuid: String, org_id: String) } #[derive(Debug, Serialize, Deserialize)] +pub struct FileDownloadClaims { + // Not before + pub nbf: i64, + // Expiration time + pub exp: i64, + // Issuer + pub iss: String, + // Subject + pub sub: String, + + pub file_id: String, +} + +pub fn generate_file_download_claims(uuid: String, file_id: String) -> FileDownloadClaims { + let time_now = Utc::now().naive_utc(); + FileDownloadClaims { + nbf: time_now.timestamp(), + exp: (time_now + Duration::minutes(5)).timestamp(), + iss: JWT_FILE_DOWNLOAD_ISSUER.to_string(), + sub: uuid, + file_id, + } +} + +#[derive(Debug, Serialize, Deserialize)] pub struct BasicJwtClaims { // Not before pub nbf: i64, diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs @@ -35,7 +35,8 @@ impl Attachment { } pub fn get_url(&self, host: &str) -> String { - format!("{}/attachments/{}/{}", host, self.cipher_uuid, self.id) + let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone())); + format!("{}/attachments/{}/{}?token={}", host, self.cipher_uuid, self.id, token) } pub fn to_json(&self, host: &str) -> Value { @@ -51,6 +52,7 @@ impl Attachment { } } +use crate::auth::{encode_jwt, generate_file_download_claims}; use crate::db::DbConn; use crate::api::EmptyResult; diff --git a/src/util.rs b/src/util.rs @@ -1,7 +1,10 @@ // // Web Headers and caching // -use std::io::{Cursor, ErrorKind}; +use std::{ + io::{Cursor, ErrorKind}, + ops::Deref, +}; use rocket::{ fairing::{Fairing, Info, Kind}, @@ -209,6 +212,14 @@ impl std::fmt::Display for SafeString { } } +impl Deref for SafeString { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl AsRef<Path> for SafeString { #[inline] fn as_ref(&self) -> &Path {