mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-11 08:47:46 +02:00
* s3api: reject SSE headers PutObject cannot honor, as CopyObject does PutObject and CreateMultipartUpload did not check the server-side encryption headers they were given. An x-amz-server-side-encryption value that names no method, such as "aes:kms", matched none of the SSE paths, so the object was stored unencrypted and the request answered 200. SSE-C together with x-amz-server-side-encryption was also accepted, and one of the two silently won. CopyObject already rejects both through validateEncryptionCompatibility. Run the same check, with the same error codes, before PutObject and CreateMultipartUpload store anything. * s3api: answer rejected SSE headers with InvalidArgument, as S3 does S3 rejects an unknown x-amz-server-side-encryption value and SSE-C combined with another method with InvalidArgument. PutObject and CreateMultipartUpload now return that code, with S3's messages, through two new error codes; CopyObject keeps its own. Also run ceph/s3-tests' test_put_obj_enc_conflict_c_s3, _c_kms and _bad_enc_kms in CI, which check both handlers' responses end to end. * s3api: close the remaining ways a PUT could skip requested encryption - A repeated x-amz-server-side-encryption header is rejected: the encryption paths apply only the first value, so extra values could hide the method the client asked for. - KMS options (key id, encryption context, bucket key) are rejected unless the method is aws:kms, matching the S3 InvalidArgument error. - CreateMultipartUpload validates before auto-create, so a refused upload cannot leave a bucket behind. - Directory markers encrypt their inline content through the shared SSE path and record the same entry metadata as regular objects, instead of storing requested-encrypted bytes in plaintext. * s3api: read back what the SSE marker write stores - Repeated SSE-C and KMS option headers are rejected alongside a repeated x-amz-server-side-encryption, closing the same first-value bypass for customer-key and KMS fields. - Algorithm validity is checked before KMS options so an unsupported value keeps the "not supported" error. - serveDirectoryContent decrypts marker content with the stored SSE metadata and returns the SSE headers, so an encrypted marker reads back what was written; its Content-Length now reflects the bytes actually served. * s3api: HEAD of a marker skips decryption and keeps the stored size HEAD returns no body, so it now runs only the SSE-C key check instead of decrypting — matching HeadObjectHandler and avoiding a KMS round-trip — and chunk-backed directory entries report Attributes.FileSize again rather than the length of their (empty) inline content. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3api: keep GET Content-Length to the bytes serveDirectoryContent writes The FileSize override described chunk-backed markers on HEAD, but GET sends only the inline content, so it promised bytes it never wrote. * s3api: stream a chunk-backed directory on GET like any object A directory promoted over an uploaded object keeps the object chunks with empty inline content, so serving only entry.Content made GET deliver nothing while HEAD reported FileSize. GET now routes those entries through the regular volume-server stream, keeping the two methods consistent. * s3api: answer a busy volume read with RequestBytesExceed --------- Co-authored-by: Chris Lu <chris.lu@gmail.com> Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
381 lines
11 KiB
Go
381 lines
11 KiB
Go
package s3api
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
)
|
|
|
|
// CopyValidationError represents validation errors during copy operations
|
|
type CopyValidationError struct {
|
|
Code s3err.ErrorCode
|
|
Message string
|
|
}
|
|
|
|
func (e *CopyValidationError) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
// ValidateCopyEncryption performs comprehensive validation of copy encryption parameters
|
|
func ValidateCopyEncryption(srcMetadata map[string][]byte, headers http.Header) error {
|
|
// Validate SSE-C copy requirements
|
|
if err := validateSSECCopyRequirements(srcMetadata, headers); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate SSE-KMS copy requirements
|
|
if err := validateSSEKMSCopyRequirements(srcMetadata, headers); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate incompatible encryption combinations
|
|
if err := validateEncryptionCompatibility(headers); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateSSECCopyRequirements validates SSE-C copy header requirements
|
|
func validateSSECCopyRequirements(srcMetadata map[string][]byte, headers http.Header) error {
|
|
srcIsSSEC := IsSSECEncrypted(srcMetadata)
|
|
hasCopyHeaders := hasSSECCopyHeaders(headers)
|
|
hasSSECHeaders := hasSSECHeaders(headers)
|
|
|
|
// If source is SSE-C encrypted, copy headers are required
|
|
if srcIsSSEC && !hasCopyHeaders {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "SSE-C encrypted source requires copy source encryption headers",
|
|
}
|
|
}
|
|
|
|
// If copy headers are provided, source must be SSE-C encrypted
|
|
if hasCopyHeaders && !srcIsSSEC {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "SSE-C copy headers provided but source is not SSE-C encrypted",
|
|
}
|
|
}
|
|
|
|
// Validate copy header completeness
|
|
if hasCopyHeaders {
|
|
if err := validateSSECCopyHeaderCompleteness(headers); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Validate destination SSE-C headers if present
|
|
if hasSSECHeaders {
|
|
if err := validateSSECHeaderCompleteness(headers); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateSSEKMSCopyRequirements validates SSE-KMS copy requirements
|
|
func validateSSEKMSCopyRequirements(srcMetadata map[string][]byte, headers http.Header) error {
|
|
dstIsSSEKMS := IsSSEKMSRequest(&http.Request{Header: headers})
|
|
|
|
// Validate KMS key ID format if provided
|
|
if dstIsSSEKMS {
|
|
keyID := headers.Get(s3_constants.AmzServerSideEncryptionAwsKmsKeyId)
|
|
if keyID != "" && !isValidKMSKeyID(keyID) {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrKMSKeyNotFound,
|
|
Message: fmt.Sprintf("Invalid KMS key ID format: %s", keyID),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate encryption context format if provided
|
|
if contextHeader := headers.Get(s3_constants.AmzServerSideEncryptionContext); contextHeader != "" {
|
|
if !dstIsSSEKMS {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "Encryption context can only be used with SSE-KMS",
|
|
}
|
|
}
|
|
|
|
// Validate base64 encoding and JSON format
|
|
if err := validateEncryptionContext(contextHeader); err != nil {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: fmt.Sprintf("Invalid encryption context: %v", err),
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateEncryptionCompatibility validates that encryption methods are not conflicting
|
|
func validateEncryptionCompatibility(headers http.Header) error {
|
|
// A repeated header is rejected rather than deduped: the encryption paths
|
|
// apply only the first value, so extra values could hide the method the
|
|
// client actually asked for.
|
|
for _, name := range []string{
|
|
s3_constants.AmzServerSideEncryption,
|
|
s3_constants.AmzServerSideEncryptionCustomerAlgorithm,
|
|
s3_constants.AmzServerSideEncryptionCustomerKey,
|
|
s3_constants.AmzServerSideEncryptionCustomerKeyMD5,
|
|
s3_constants.AmzServerSideEncryptionAwsKmsKeyId,
|
|
s3_constants.AmzServerSideEncryptionContext,
|
|
s3_constants.AmzServerSideEncryptionBucketKeyEnabled,
|
|
} {
|
|
if len(headers.Values(name)) > 1 {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: fmt.Sprintf("Multiple %s headers specified - only one is allowed", name),
|
|
}
|
|
}
|
|
}
|
|
|
|
sseAlgorithm := headers.Get(s3_constants.AmzServerSideEncryption)
|
|
hasSSEC := hasSSECHeaders(headers)
|
|
hasSSEKMS := sseAlgorithm == s3_constants.SSEAlgorithmKMS
|
|
hasSSES3 := sseAlgorithm == s3_constants.SSEAlgorithmAES256
|
|
|
|
// Reject unsupported algorithms so they are never persisted as a bogus
|
|
// destination header advertising encryption that was never applied.
|
|
if sseAlgorithm != "" && !hasSSEKMS && !hasSSES3 {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidEncryptionAlgorithm,
|
|
Message: fmt.Sprintf("Unsupported server-side encryption algorithm: %s", sseAlgorithm),
|
|
}
|
|
}
|
|
|
|
// KMS options only apply to aws:kms; with another method or none they name
|
|
// no method at all.
|
|
if !hasSSEKMS && hasHeaderValue(headers,
|
|
s3_constants.AmzServerSideEncryptionAwsKmsKeyId,
|
|
s3_constants.AmzServerSideEncryptionContext,
|
|
s3_constants.AmzServerSideEncryptionBucketKeyEnabled) {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "KMS encryption options require the aws:kms encryption method",
|
|
}
|
|
}
|
|
|
|
// Only one encryption method should be specified
|
|
encryptionCount := 0
|
|
for _, specified := range []bool{hasSSEC, hasSSEKMS, hasSSES3} {
|
|
if specified {
|
|
encryptionCount++
|
|
}
|
|
}
|
|
if encryptionCount > 1 {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "Multiple encryption methods specified - only one is allowed",
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateRequestEncryption rejects PutObject or CreateMultipartUpload
|
|
// encryption headers that no single method can honor, with the InvalidArgument
|
|
// codes S3 returns for them.
|
|
func ValidateRequestEncryption(headers http.Header) s3err.ErrorCode {
|
|
err := validateEncryptionCompatibility(headers)
|
|
if err == nil {
|
|
return s3err.ErrNone
|
|
}
|
|
var validationErr *CopyValidationError
|
|
if errors.As(err, &validationErr) && validationErr.Code == s3err.ErrInvalidEncryptionAlgorithm {
|
|
return s3err.ErrInvalidEncryptionMethod
|
|
}
|
|
return s3err.ErrIncompatibleEncryptionMethod
|
|
}
|
|
|
|
// validateSSECCopyHeaderCompleteness validates that all required SSE-C copy headers are present
|
|
func validateSSECCopyHeaderCompleteness(headers http.Header) error {
|
|
algorithm := headers.Get(s3_constants.AmzCopySourceServerSideEncryptionCustomerAlgorithm)
|
|
key := headers.Get(s3_constants.AmzCopySourceServerSideEncryptionCustomerKey)
|
|
keyMD5 := headers.Get(s3_constants.AmzCopySourceServerSideEncryptionCustomerKeyMD5)
|
|
|
|
if algorithm == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "SSE-C copy customer algorithm header is required",
|
|
}
|
|
}
|
|
|
|
if key == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "SSE-C copy customer key header is required",
|
|
}
|
|
}
|
|
|
|
if keyMD5 == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "SSE-C copy customer key MD5 header is required",
|
|
}
|
|
}
|
|
|
|
// Validate algorithm
|
|
if algorithm != "AES256" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: fmt.Sprintf("Unsupported SSE-C algorithm: %s", algorithm),
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateSSECHeaderCompleteness validates that all required SSE-C headers are present
|
|
func validateSSECHeaderCompleteness(headers http.Header) error {
|
|
algorithm := headers.Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm)
|
|
key := headers.Get(s3_constants.AmzServerSideEncryptionCustomerKey)
|
|
keyMD5 := headers.Get(s3_constants.AmzServerSideEncryptionCustomerKeyMD5)
|
|
|
|
if algorithm == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "SSE-C customer algorithm header is required",
|
|
}
|
|
}
|
|
|
|
if key == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "SSE-C customer key header is required",
|
|
}
|
|
}
|
|
|
|
if keyMD5 == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "SSE-C customer key MD5 header is required",
|
|
}
|
|
}
|
|
|
|
// Validate algorithm
|
|
if algorithm != "AES256" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: fmt.Sprintf("Unsupported SSE-C algorithm: %s", algorithm),
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Helper functions for header detection
|
|
func hasHeaderValue(headers http.Header, names ...string) bool {
|
|
for _, name := range names {
|
|
for _, value := range headers.Values(name) {
|
|
if value != "" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func hasSSECCopyHeaders(headers http.Header) bool {
|
|
return hasHeaderValue(headers,
|
|
s3_constants.AmzCopySourceServerSideEncryptionCustomerAlgorithm,
|
|
s3_constants.AmzCopySourceServerSideEncryptionCustomerKey,
|
|
s3_constants.AmzCopySourceServerSideEncryptionCustomerKeyMD5)
|
|
}
|
|
|
|
func hasSSECHeaders(headers http.Header) bool {
|
|
return hasHeaderValue(headers,
|
|
s3_constants.AmzServerSideEncryptionCustomerAlgorithm,
|
|
s3_constants.AmzServerSideEncryptionCustomerKey,
|
|
s3_constants.AmzServerSideEncryptionCustomerKeyMD5)
|
|
}
|
|
|
|
// validateEncryptionContext validates the encryption context header format
|
|
func validateEncryptionContext(contextHeader string) error {
|
|
// This would validate base64 encoding and JSON format
|
|
// Implementation would decode base64 and parse JSON
|
|
// For now, just check it's not empty
|
|
if contextHeader == "" {
|
|
return fmt.Errorf("encryption context cannot be empty")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateCopySource validates the copy source path.
|
|
func ValidateCopySource(copySource string, srcBucket, srcObject string) error {
|
|
return validateCopySource(copySource, srcBucket, srcObject, "")
|
|
}
|
|
|
|
func validateCopySource(copySource string, srcBucket, srcObject, srcVersionId string) error {
|
|
if copySource == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidCopySource,
|
|
Message: "Copy source header is required",
|
|
}
|
|
}
|
|
|
|
if srcBucket == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidCopySource,
|
|
Message: "Source bucket cannot be empty",
|
|
}
|
|
}
|
|
|
|
if srcObject == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidCopySource,
|
|
Message: "Source object cannot be empty",
|
|
}
|
|
}
|
|
|
|
// `.`/`..` segments are collapsed by the filer's path join; reject them as
|
|
// IsValidObjectKey does for the request URL so the source stays in-bucket.
|
|
if !s3_constants.IsValidBucketName(srcBucket) || !s3_constants.IsValidObjectKey(srcObject) {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidCopySource,
|
|
Message: "Copy source contains invalid path segments",
|
|
}
|
|
}
|
|
if !isValidVersionID(srcVersionId) {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidCopySource,
|
|
Message: "Copy source contains an invalid version ID",
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateCopyDestination validates the copy destination
|
|
func ValidateCopyDestination(dstBucket, dstObject string) error {
|
|
if dstBucket == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "Destination bucket cannot be empty",
|
|
}
|
|
}
|
|
|
|
if dstObject == "" {
|
|
return &CopyValidationError{
|
|
Code: s3err.ErrInvalidRequest,
|
|
Message: "Destination object cannot be empty",
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MapCopyValidationError maps validation errors to appropriate S3 error codes
|
|
func MapCopyValidationError(err error) s3err.ErrorCode {
|
|
if validationErr, ok := err.(*CopyValidationError); ok {
|
|
return validationErr.Code
|
|
}
|
|
return s3err.ErrInvalidRequest
|
|
}
|