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 <housemecn@gmail.com>
This commit is contained in:
安正超
2026-06-11 13:48:53 +08:00
committed by GitHub
parent d4ee44f49f
commit d60deba9b6
2 changed files with 22 additions and 4 deletions
+20
View File
@@ -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
+2 -4
View File
@@ -318,11 +318,9 @@ pub struct ListRemoteTargetHandler {}
#[async_trait::async_trait]
impl Operation for ListRemoteTargetHandler {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
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() {