From d60deba9b6c6b47049b91bd57d90a327b92e0617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=89=E6=AD=A3=E8=B6=85?= Date: Thu, 11 Jun 2026 13:48:53 +0800 Subject: [PATCH] fix(security): add IAM authorization to FTP read/metadata/cwd handlers (GHSA-3g29-xff2-92vp) (#3347) fix(security): add IAM authorization to FTP read/metadata/cwd handlers The FTP frontend's get() (RETR), metadata() (SIZE/MDTM), and cwd() (CWD) handlers dispatched directly to the storage backend without calling authorize_operation(). This allowed any authenticated FTP user, including those with explicit Deny policies, to read arbitrary objects and probe bucket existence regardless of IAM policy. Add authorize_operation() calls matching the pattern used by the write-path handlers (put, del, list, rmd) and the WebDAV driver: - get(): S3Action::GetObject - metadata() for files: S3Action::HeadObject - metadata() for directories: S3Action::HeadBucket - cwd(): S3Action::HeadBucket Fixes: GHSA-3g29-xff2-92vp Co-authored-by: houseme --- crates/protocols/src/ftps/driver.rs | 20 ++++++++++++++++++++ rustfs/src/admin/handlers/replication.rs | 6 ++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/crates/protocols/src/ftps/driver.rs b/crates/protocols/src/ftps/driver.rs index 02ed09fbd..3ca01b348 100644 --- a/crates/protocols/src/ftps/driver.rs +++ b/crates/protocols/src/ftps/driver.rs @@ -233,6 +233,11 @@ where .map_err(|e| Error::new(ErrorKind::PermanentFileNotAvailable, format!("{}: {}", "Invalid path", e)))?; if let Some(key) = key { + // Authorize HeadObject + authorize_operation(session_context, &S3Action::HeadObject, &bucket, Some(&key)) + .await + .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied"))?; + match self .storage .head_object( @@ -264,6 +269,11 @@ where } } else { // Directory metadata - use HeadBucket + // Authorize HeadBucket + authorize_operation(session_context, &S3Action::HeadBucket, &bucket, None) + .await + .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied"))?; + let bucket_clone = bucket.clone(); match self .storage @@ -434,6 +444,11 @@ where let key = key.ok_or_else(|| Error::new(ErrorKind::PermanentFileNotAvailable, "Cannot get directory"))?; + // Authorize GetObject + authorize_operation(session_context, &S3Action::GetObject, &bucket, Some(&key)) + .await + .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied"))?; + match self .storage .get_object( @@ -668,6 +683,11 @@ where .parse_s3_path(&path_str) .map_err(|e| Error::new(ErrorKind::PermanentFileNotAvailable, format!("{}: {}", "Invalid path", e)))?; + // Authorize HeadBucket (CWD probes bucket existence) + authorize_operation(session_context, &S3Action::HeadBucket, &bucket, None) + .await + .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied"))?; + // Check if bucket exists match self .storage diff --git a/rustfs/src/admin/handlers/replication.rs b/rustfs/src/admin/handlers/replication.rs index 0d9618ab3..dc357d276 100644 --- a/rustfs/src/admin/handlers/replication.rs +++ b/rustfs/src/admin/handlers/replication.rs @@ -318,11 +318,9 @@ pub struct ListRemoteTargetHandler {} #[async_trait::async_trait] impl Operation for ListRemoteTargetHandler { async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + validate_replication_admin_request(&req, AdminAction::GetBucketTargetAction).await?; + let queries = extract_query_params(&req.uri); - let Some(_cred) = req.credentials else { - error!("credentials null"); - return Err(s3_error!(InvalidRequest, "get cred failed")); - }; if let Some(bucket) = queries.get("bucket") { if bucket.is_empty() {