fix(security): patch critical IDOR & auth vulnerabilities
- Fix logout no-op: extract refresh token from cookie/body (auth_handler) - Secure all 12 WebDAV handlers with AuthUser + resolve_path_for_user - Secure all 7 batch handlers with caller_id ownership checks - Add _owned variants: copy_file_owned, delete_file_owned, get_file_stream_owned, get_folder_owned - Secure list_files_query: add AuthUser, SQL-level user_id filter, tenant-isolated ETag - Remove deprecated unscoped resolve_path() and exists() from PathResolverService - Remove dead list_files handler (unmounted, no auth) - Add list_files_for_owner (SQL) and list_files_owned across trait chain
This commit is contained in:
@@ -119,12 +119,29 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
|
||||
/// Lists files in a folder
|
||||
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<FileDto>, DomainError>;
|
||||
|
||||
/// Lists files in a folder, scoped to the authenticated user.
|
||||
///
|
||||
/// Uses SQL-level `AND user_id` filtering — no in-memory post-filter.
|
||||
/// All user-facing list handlers should use this method.
|
||||
async fn list_files_owned(
|
||||
&self,
|
||||
folder_id: Option<&str>,
|
||||
owner_id: &str,
|
||||
) -> Result<Vec<FileDto>, DomainError>;
|
||||
|
||||
/// Gets file content as a stream (for large files)
|
||||
async fn get_file_stream(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
||||
|
||||
/// Gets file content as a stream, enforcing that `caller_id` is the owner.
|
||||
async fn get_file_stream_owned(
|
||||
&self,
|
||||
id: &str,
|
||||
caller_id: &str,
|
||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
||||
|
||||
/// Optimized multi-tier download.
|
||||
///
|
||||
/// Internalises: write-behind lookup → content-cache → WebP transcode →
|
||||
@@ -208,6 +225,24 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
|
||||
.take(limit as usize)
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Like [`list_files_batch`], but scoped to a specific owner.
|
||||
///
|
||||
/// Used by streaming WebDAV PROPFIND so that each user only sees their
|
||||
/// own files, even in shared folder_id namespaces.
|
||||
async fn list_files_batch_for_owner(
|
||||
&self,
|
||||
folder_id: Option<&str>,
|
||||
owner_id: &str,
|
||||
offset: i64,
|
||||
limit: i64,
|
||||
) -> Result<Vec<FileDto>, DomainError> {
|
||||
let all = self.list_files_batch(folder_id, offset, limit).await?;
|
||||
Ok(all
|
||||
.into_iter()
|
||||
.filter(|f| f.owner_id.as_deref().map_or(false, |o| o == owner_id))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
@@ -238,6 +273,14 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
|
||||
target_folder_id: Option<String>,
|
||||
) -> Result<FileDto, DomainError>;
|
||||
|
||||
/// Copies a file, enforcing that `caller_id` is the owner.
|
||||
async fn copy_file_owned(
|
||||
&self,
|
||||
file_id: &str,
|
||||
caller_id: &str,
|
||||
target_folder_id: Option<String>,
|
||||
) -> Result<FileDto, DomainError>;
|
||||
|
||||
/// Renames a file (system/internal — no ownership check).
|
||||
async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<FileDto, DomainError>;
|
||||
|
||||
@@ -252,6 +295,9 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
|
||||
/// Deletes a file (system/internal — no ownership check).
|
||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Deletes a file, enforcing that `caller_id` is the owner.
|
||||
async fn delete_file_owned(&self, id: &str, caller_id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Smart delete: trash-first with dedup reference cleanup.
|
||||
///
|
||||
/// 1. Tries to move to trash (soft delete).
|
||||
|
||||
@@ -16,6 +16,12 @@ pub trait FolderUseCase: Send + Sync + 'static {
|
||||
/// Gets a folder by its ID
|
||||
async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError>;
|
||||
|
||||
/// Gets a folder by its ID, enforcing that `caller_id` is the owner.
|
||||
///
|
||||
/// Returns `NotFound` if the folder does not exist **or** belongs to
|
||||
/// another user. All user-facing handlers should use this method.
|
||||
async fn get_folder_owned(&self, id: &str, caller_id: &str) -> Result<FolderDto, DomainError>;
|
||||
|
||||
/// Gets a folder by its path
|
||||
async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError>;
|
||||
|
||||
|
||||
@@ -46,6 +46,22 @@ pub trait FileReadPort: Send + Sync + 'static {
|
||||
/// Lists files in a folder.
|
||||
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, DomainError>;
|
||||
|
||||
/// Lists files in a folder scoped to a specific owner (SQL-level).
|
||||
///
|
||||
/// Default falls back to `list_files` + in-memory filter.
|
||||
/// Repositories should override with a direct `AND user_id = $N` query.
|
||||
async fn list_files_for_owner(
|
||||
&self,
|
||||
folder_id: Option<&str>,
|
||||
owner_id: &str,
|
||||
) -> Result<Vec<File>, DomainError> {
|
||||
let all = self.list_files(folder_id).await?;
|
||||
Ok(all
|
||||
.into_iter()
|
||||
.filter(|f| f.owner_id().map_or(false, |o| o == owner_id))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Gets content as a stream (ideal for large files).
|
||||
async fn get_file_stream(
|
||||
&self,
|
||||
@@ -108,6 +124,25 @@ pub trait FileReadPort: Send + Sync + 'static {
|
||||
Ok(all.into_iter().skip(start).take(end - start).collect())
|
||||
}
|
||||
|
||||
/// Like [`list_files_batch`], but only returns files owned by `owner_id`.
|
||||
///
|
||||
/// Used by streaming WebDAV PROPFIND to list files scoped to the
|
||||
/// authenticated user, preventing cross-user data leakage.
|
||||
async fn list_files_batch_for_owner(
|
||||
&self,
|
||||
folder_id: Option<&str>,
|
||||
owner_id: &str,
|
||||
offset: i64,
|
||||
limit: i64,
|
||||
) -> Result<Vec<File>, DomainError> {
|
||||
// Default: filter in-memory (repos should override with SQL)
|
||||
let all = self.list_files_batch(folder_id, offset, limit).await?;
|
||||
Ok(all
|
||||
.into_iter()
|
||||
.filter(|f| f.owner_id().map_or(false, |o| o == owner_id))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Streams every file in the subtree rooted at `folder_id`.
|
||||
///
|
||||
/// Uses an ltree `<@` join against `storage.folders` so the entire
|
||||
|
||||
Reference in New Issue
Block a user