From e313276e49ff66f0a42a98911718ad81bbfed30e Mon Sep 17 00:00:00 2001 From: Zhengchao An Date: Thu, 13 Aug 2026 03:42:03 +0800 Subject: [PATCH] fix(sse): align copy-path unknown-algorithm fallback with put path (#6022) --- rustfs/src/app/object_usecase.rs | 73 ++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/rustfs/src/app/object_usecase.rs b/rustfs/src/app/object_usecase.rs index a6a656ac1..edfe7df83 100644 --- a/rustfs/src/app/object_usecase.rs +++ b/rustfs/src/app/object_usecase.rs @@ -162,8 +162,9 @@ use s3s::dto::{ GetObjectInput, GetObjectOutput, HeadObjectInput, HeadObjectOutput, MetadataDirective, ObjectAttributes, ObjectLockLegalHold, ObjectLockLegalHoldStatus, ObjectLockMode, ObjectLockRetention, ObjectLockRetentionMode, ObjectPart, PutObjectInput, PutObjectOutput, Range, RequestCharged, RestoreObjectInput, RestoreObjectOutput, RestoreStatus, SSECustomerAlgorithm, - SSECustomerKeyMD5, SSEKMSKeyId, SelectObjectContentInput, SelectObjectContentOutput, ServerSideEncryption, StorageClass, - StreamingBlob, TaggingDirective, TaggingHeader, Timestamp, TimestampFormat, WebsiteRedirectLocation, + SSECustomerKeyMD5, SSEKMSKeyId, SelectObjectContentInput, SelectObjectContentOutput, ServerSideEncryption, + ServerSideEncryptionByDefault, StorageClass, StreamingBlob, TaggingDirective, TaggingHeader, Timestamp, TimestampFormat, + WebsiteRedirectLocation, }; use s3s::header::{X_AMZ_RESTORE, X_AMZ_RESTORE_OUTPUT_PATH}; use s3s::stream::{ByteStream, DynByteStream, RemainingLength}; @@ -2568,6 +2569,25 @@ fn has_put_sse_request_headers(headers: &HeaderMap) -> bool { || headers.get(AMZ_SERVER_SIDE_ENCRYPTION_KMS_ID).is_some() } +/// Managed SSE resolved from a bucket default encryption rule on the copy path. +/// +/// Unknown algorithms fall back to AES256, the same total mapping as the PUT and +/// extract paths and the storage-layer resolver (`prepare_sse_configuration`), which +/// `sse_encryption` re-runs when it mints the destination DEK. Resolving `None` here +/// instead lets a same-name copy under a malformed bucket default pass the +/// `copy_changes_encryption` guard and take the metadata-only shortcut while the +/// storage layer still encrypts: fresh DEK metadata is committed beside the untouched +/// plaintext blocks and the object becomes unreadable. Reachable only via corrupt or +/// hand-edited bucket metadata — PutBucketEncryption rejects unknown algorithms +/// (backlog#1826). +fn bucket_default_write_sse(sse: &ServerSideEncryptionByDefault) -> ServerSideEncryption { + match sse.sse_algorithm.as_str() { + "AES256" => ServerSideEncryption::from_static(ServerSideEncryption::AES256), + "aws:kms" => ServerSideEncryption::from_static(ServerSideEncryption::AWS_KMS), + _ => ServerSideEncryption::from_static(ServerSideEncryption::AES256), + } +} + fn should_use_small_eager_put_path( size: i64, headers: &HeaderMap, @@ -7181,11 +7201,7 @@ impl DefaultObjectUsecase { config.rules.first().and_then(|rule| { rule.apply_server_side_encryption_by_default .as_ref() - .and_then(|sse| match sse.sse_algorithm.as_str() { - "AES256" => Some(ServerSideEncryption::from_static(ServerSideEncryption::AES256)), - "aws:kms" => Some(ServerSideEncryption::from_static(ServerSideEncryption::AWS_KMS)), - _ => None, - }) + .map(bucket_default_write_sse) }) }) }); @@ -9575,7 +9591,8 @@ mod tests { DefaultRetention, Delete, DeleteMarkerReplication, DeleteMarkerReplicationStatus, DeleteReplication, DeleteReplicationStatus, Destination, ExistingObjectReplication, ExistingObjectReplicationStatus, ObjectIdentifier, ObjectLockConfiguration, ObjectLockEnabled, ObjectLockRule, ReplicaModifications, ReplicaModificationsStatus, - ReplicationConfiguration, ReplicationRule, ReplicationRuleStatus, RestoreRequest, SourceSelectionCriteria, + ReplicationConfiguration, ReplicationRule, ReplicationRuleStatus, RestoreRequest, ServerSideEncryptionConfiguration, + ServerSideEncryptionRule, SourceSelectionCriteria, }; use std::pin::Pin; use std::sync::Arc; @@ -9770,6 +9787,46 @@ mod tests { assert!(lookup_opts.no_lock); } + // A malformed bucket-default algorithm reaches this resolution only through + // corrupt or hand-edited bucket metadata (PutBucketEncryption validates the + // value), so the invariant is pinned here rather than end-to-end: the copy + // path must resolve managed AES256 exactly like PUT/extract. With an + // unencrypted same-name source and no SSE-C, the resolved default alone + // keeps `copy_changes_encryption` true, so the metadata-only shortcut stays + // off while `sse_encryption` mints a fresh DEK (backlog#1826). + #[test] + fn copy_bucket_default_unknown_sse_algorithm_falls_back_to_aes256() { + let config = ServerSideEncryptionConfiguration { + rules: vec![ServerSideEncryptionRule { + apply_server_side_encryption_by_default: Some(ServerSideEncryptionByDefault { + sse_algorithm: ServerSideEncryption::from(String::from("garbage")), + kms_master_key_id: None, + }), + bucket_key_enabled: None, + }], + }; + + let effective_sse = config + .rules + .first() + .and_then(|rule| rule.apply_server_side_encryption_by_default.as_ref()) + .map(bucket_default_write_sse); + + assert_eq!(effective_sse.as_ref().map(|sse| sse.as_str()), Some(ServerSideEncryption::AES256)); + + // Valid algorithms map to themselves, byte-identical to the PUT path. + for (configured, expected) in [ + (ServerSideEncryption::AES256, ServerSideEncryption::AES256), + (ServerSideEncryption::AWS_KMS, ServerSideEncryption::AWS_KMS), + ] { + let sse = ServerSideEncryptionByDefault { + sse_algorithm: ServerSideEncryption::from_static(configured), + kms_master_key_id: None, + }; + assert_eq!(bucket_default_write_sse(&sse).as_str(), expected); + } + } + #[test] fn put_request_user_metadata_cannot_suppress_bucket_default_retention() { let mut metadata =