From 28fe92065a7ffa20de38eff0f907782c7900dcb1 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Tue, 24 Mar 2026 18:11:51 -0700 Subject: [PATCH] S3: reject part uploads after AbortMultipartUpload (#8768) * S3: reject part uploads after AbortMultipartUpload PutObjectPartHandler did not verify that the multipart upload session still exists before accepting parts. After AbortMultipartUpload deleted the upload directory, the ErrNotFound from getEntry was silently ignored (treated as "may be non-SSE upload"), allowing parts to be stored as orphaned files. Now return ErrNoSuchUpload when the upload directory is not found, matching AWS S3 behavior. Fixes #8766 * S3: check upload existence unconditionally in PutObjectPartHandler Move the getEntry call out of the SSE-type conditional so the upload existence check runs for all part uploads, including SSE-C. Previously the SSE-C path skipped the check entirely, allowing parts to be uploaded after abort when SSE-C headers were present. Also flattens the nested SSE branching by one level now that getEntry is called once upfront. * S3: address PR review feedback for PutObjectPartHandler - Log at error level when getEntry fails with an unexpected error, since we return ErrInternalError to the client - Distinguish base IV decode errors from length validation failures with separate, clearer error messages --------- Co-authored-by: Copilot --- weed/s3api/s3api_object_handlers_multipart.go | 135 +++++++++--------- 1 file changed, 64 insertions(+), 71 deletions(-) diff --git a/weed/s3api/s3api_object_handlers_multipart.go b/weed/s3api/s3api_object_handlers_multipart.go index ceb1d4eb8..46c249658 100644 --- a/weed/s3api/s3api_object_handlers_multipart.go +++ b/weed/s3api/s3api_object_handlers_multipart.go @@ -366,82 +366,75 @@ func (s3a *S3ApiServer) PutObjectPartHandler(w http.ResponseWriter, r *http.Requ glog.V(2).Infof("PutObjectPartHandler %s %s %04d", bucket, uploadID, partID) - // Check for SSE-C headers in the current request first + // Verify the multipart upload exists (rejects parts after abort) + uploadEntry, err := s3a.getEntry(s3a.genUploadsFolder(bucket), uploadID) + if errors.Is(err, filer_pb.ErrNotFound) { + s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchUpload) + return + } else if err != nil { + glog.Errorf("Could not retrieve upload entry for %s/%s: %v", bucket, uploadID, err) + s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) + return + } + + // Apply SSE settings from the upload entry (unless SSE-C headers are already present) sseCustomerAlgorithm := r.Header.Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm) - if sseCustomerAlgorithm != "" { - // SSE-C part upload - headers are already present, let putToFiler handle it - } else { - // No SSE-C headers, check for SSE-KMS settings from upload directory - if uploadEntry, err := s3a.getEntry(s3a.genUploadsFolder(bucket), uploadID); err == nil { - if uploadEntry.Extended != nil { - // Check if this upload uses SSE-KMS - if keyIDBytes, exists := uploadEntry.Extended[s3_constants.SeaweedFSSSEKMSKeyID]; exists { - keyID := string(keyIDBytes) - - // Build SSE-KMS metadata for this part - bucketKeyEnabled := false - if bucketKeyBytes, exists := uploadEntry.Extended[s3_constants.SeaweedFSSSEKMSBucketKeyEnabled]; exists && string(bucketKeyBytes) == "true" { - bucketKeyEnabled = true - } - - var encryptionContext map[string]string - if contextBytes, exists := uploadEntry.Extended[s3_constants.SeaweedFSSSEKMSEncryptionContext]; exists { - // Parse the stored encryption context - if err := json.Unmarshal(contextBytes, &encryptionContext); err != nil { - glog.Errorf("Failed to parse encryption context for upload %s: %v", uploadID, err) - encryptionContext = BuildEncryptionContext(bucket, object, bucketKeyEnabled) - } - } else { - encryptionContext = BuildEncryptionContext(bucket, object, bucketKeyEnabled) - } - - // Get the base IV for this multipart upload - var baseIV []byte - if baseIVBytes, exists := uploadEntry.Extended[s3_constants.SeaweedFSSSEKMSBaseIV]; exists { - // Decode the base64 encoded base IV - decodedIV, decodeErr := base64.StdEncoding.DecodeString(string(baseIVBytes)) - if decodeErr == nil && len(decodedIV) == s3_constants.AESBlockSize { - baseIV = decodedIV - glog.V(4).Infof("Using stored base IV %x for multipart upload %s", baseIV[:8], uploadID) - } else { - glog.Errorf("Failed to decode base IV for multipart upload %s: %v (expected %d bytes, got %d)", uploadID, decodeErr, s3_constants.AESBlockSize, len(decodedIV)) - } - } - - // Base IV is required for SSE-KMS multipart uploads - fail if missing or invalid - if len(baseIV) == 0 { - glog.Errorf("No valid base IV found for SSE-KMS multipart upload %s - cannot proceed with encryption", uploadID) - s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) - return - } - - // Add SSE-KMS headers to the request for putToFiler to handle encryption - r.Header.Set(s3_constants.AmzServerSideEncryption, "aws:kms") - r.Header.Set(s3_constants.AmzServerSideEncryptionAwsKmsKeyId, keyID) - if bucketKeyEnabled { - r.Header.Set(s3_constants.AmzServerSideEncryptionBucketKeyEnabled, "true") - } - if len(encryptionContext) > 0 { - if contextJSON, err := json.Marshal(encryptionContext); err == nil { - r.Header.Set(s3_constants.AmzServerSideEncryptionContext, base64.StdEncoding.EncodeToString(contextJSON)) - } - } - - // Pass the base IV to putToFiler via header - r.Header.Set(s3_constants.SeaweedFSSSEKMSBaseIVHeader, base64.StdEncoding.EncodeToString(baseIV)) + if sseCustomerAlgorithm == "" && uploadEntry.Extended != nil { + if keyIDBytes, exists := uploadEntry.Extended[s3_constants.SeaweedFSSSEKMSKeyID]; exists { + keyID := string(keyIDBytes) + + bucketKeyEnabled := false + if bucketKeyBytes, exists := uploadEntry.Extended[s3_constants.SeaweedFSSSEKMSBucketKeyEnabled]; exists && string(bucketKeyBytes) == "true" { + bucketKeyEnabled = true + } + var encryptionContext map[string]string + if contextBytes, exists := uploadEntry.Extended[s3_constants.SeaweedFSSSEKMSEncryptionContext]; exists { + if err := json.Unmarshal(contextBytes, &encryptionContext); err != nil { + glog.Errorf("Failed to parse encryption context for upload %s: %v", uploadID, err) + encryptionContext = BuildEncryptionContext(bucket, object, bucketKeyEnabled) + } + } else { + encryptionContext = BuildEncryptionContext(bucket, object, bucketKeyEnabled) + } + + var baseIV []byte + if baseIVBytes, exists := uploadEntry.Extended[s3_constants.SeaweedFSSSEKMSBaseIV]; exists { + decodedIV, decodeErr := base64.StdEncoding.DecodeString(string(baseIVBytes)) + if decodeErr != nil { + glog.Errorf("Failed to decode base IV for multipart upload %s: %v", uploadID, decodeErr) + } else if len(decodedIV) != s3_constants.AESBlockSize { + glog.Errorf("Invalid base IV length for multipart upload %s: expected %d bytes, got %d", uploadID, s3_constants.AESBlockSize, len(decodedIV)) } else { - // Check if this upload uses SSE-S3 - if err := s3a.handleSSES3MultipartHeaders(r, uploadEntry, uploadID); err != nil { - glog.Errorf("Failed to setup SSE-S3 multipart headers: %v", err) - s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) - return - } + baseIV = decodedIV + glog.V(4).Infof("Using stored base IV %x for multipart upload %s", baseIV[:8], uploadID) + } + } + + if len(baseIV) == 0 { + glog.Errorf("No valid base IV found for SSE-KMS multipart upload %s - cannot proceed with encryption", uploadID) + s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) + return + } + + r.Header.Set(s3_constants.AmzServerSideEncryption, "aws:kms") + r.Header.Set(s3_constants.AmzServerSideEncryptionAwsKmsKeyId, keyID) + if bucketKeyEnabled { + r.Header.Set(s3_constants.AmzServerSideEncryptionBucketKeyEnabled, "true") + } + if len(encryptionContext) > 0 { + if contextJSON, err := json.Marshal(encryptionContext); err == nil { + r.Header.Set(s3_constants.AmzServerSideEncryptionContext, base64.StdEncoding.EncodeToString(contextJSON)) } } - } else if !errors.Is(err, filer_pb.ErrNotFound) { - // Log unexpected errors (but not "not found" which is normal for non-SSE uploads) - glog.V(3).Infof("Could not retrieve upload entry for %s/%s: %v (may be non-SSE upload)", bucket, uploadID, err) + + r.Header.Set(s3_constants.SeaweedFSSSEKMSBaseIVHeader, base64.StdEncoding.EncodeToString(baseIV)) + } else { + if err := s3a.handleSSES3MultipartHeaders(r, uploadEntry, uploadID); err != nil { + glog.Errorf("Failed to setup SSE-S3 multipart headers: %v", err) + s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) + return + } } }