fix(security): apply 4 vulnerability fixes from security audit
1. Share password bypass (HIGH): enforce password check in get_shared_link_by_token, verify_shared_link_password now returns ShareDto only on correct password. 2. WebDAV MOVE ownership (MEDIUM): add assert_owner on destination parent folder for file moves in both PathResolver and legacy branches. 3. Path traversal defense-in-depth (LOW): add reject_path_traversal() to WebDAV, CalDAV, and CardDAV handlers rejecting '..' segments at HTTP boundary. 4. Setup race condition (LOW): atomic INSERT ... ON CONFLICT DO NOTHING in try_claim_initialization prevents duplicate admin creation.
This commit is contained in:
@@ -130,6 +130,22 @@ pub fn webdav_routes() -> Router<Arc<AppState>> {
|
||||
.route("/webdav", axum::routing::any(handle_webdav_methods_root))
|
||||
}
|
||||
|
||||
/// Reject paths that contain path-traversal segments (`.` or `..`).
|
||||
///
|
||||
/// Although deeper layers (PathResolver, StoragePath) also filter these out,
|
||||
/// blocking them at the HTTP boundary provides defense-in-depth and ensures
|
||||
/// no handler ever receives a traversal attempt.
|
||||
fn reject_path_traversal(path: &str) -> Result<(), AppError> {
|
||||
for segment in path.split('/') {
|
||||
if segment == ".." || segment == "." {
|
||||
return Err(AppError::bad_request(
|
||||
"Path must not contain '.' or '..' segments",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract the resource path from the request URI, stripping the `/webdav/` prefix
|
||||
/// and percent-decoding the result so that folder/file names with spaces and
|
||||
/// special characters match the values stored in the database.
|
||||
@@ -160,6 +176,7 @@ async fn handle_webdav_methods(
|
||||
req: Request<Body>,
|
||||
) -> Result<Response<Body>, AppError> {
|
||||
let path = extract_webdav_path(req.uri());
|
||||
reject_path_traversal(&path)?;
|
||||
handle_webdav_dispatch(state, req, path).await
|
||||
}
|
||||
|
||||
@@ -1109,6 +1126,9 @@ async fn handle_move(
|
||||
return Err(AppError::bad_request("Invalid destination URL"));
|
||||
};
|
||||
|
||||
// SECURITY: reject path-traversal in destination
|
||||
reject_path_traversal(&destination_path)?;
|
||||
|
||||
// Get services from state
|
||||
let file_retrieval_service = &state.applications.file_retrieval_service;
|
||||
let file_management_service = &state.applications.file_management_service;
|
||||
@@ -1206,6 +1226,12 @@ async fn handle_move(
|
||||
};
|
||||
|
||||
if source_parent_path != dest_parent_path {
|
||||
// SECURITY: verify destination parent belongs to caller (V-08)
|
||||
if !dest_parent_path.is_empty() {
|
||||
if let Ok(parent) = folder_service.get_folder_by_path(dest_parent_path).await {
|
||||
assert_owner(parent.owner_id.as_deref(), &user.id, dest_parent_path)?;
|
||||
}
|
||||
}
|
||||
file_management_service
|
||||
.move_file(&file.id, Some(dest_parent_path.to_string()))
|
||||
.await
|
||||
@@ -1301,6 +1327,12 @@ async fn handle_move(
|
||||
};
|
||||
|
||||
if source_parent_path != dest_parent_path {
|
||||
// SECURITY: verify destination parent belongs to caller (V-08)
|
||||
if !dest_parent_path.is_empty() {
|
||||
if let Ok(parent) = folder_service.get_folder_by_path(dest_parent_path).await {
|
||||
assert_owner(parent.owner_id.as_deref(), &user.id, dest_parent_path)?;
|
||||
}
|
||||
}
|
||||
file_management_service
|
||||
.move_file(&file.id, Some(dest_parent_path.to_string()))
|
||||
.await
|
||||
@@ -1367,6 +1399,9 @@ async fn handle_copy(
|
||||
return Err(AppError::bad_request("Invalid destination URL"));
|
||||
};
|
||||
|
||||
// SECURITY: reject path-traversal in destination
|
||||
reject_path_traversal(&destination_path)?;
|
||||
|
||||
// Get depth from Depth header
|
||||
let depth = req
|
||||
.headers()
|
||||
|
||||
Reference in New Issue
Block a user