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:
Dionisio
2026-03-05 10:30:39 +01:00
parent ee86c3a128
commit fdbb2bf60a
14 changed files with 585 additions and 174 deletions
+35
View File
@@ -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