Browse Source

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 <copilot@github.com>
pull/8756/merge
Chris Lu 21 hours ago
committed by GitHub
parent
commit
28fe92065a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 135
      weed/s3api/s3api_object_handlers_multipart.go

135
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
}
}
}

Loading…
Cancel
Save