Files
versitygw/tests/integration/PutBucketCors.go
niksis02 6fafc15d08 fix: fixes PutBucketCors CORSRules validation
Fixes #1870
Fixes #1863

A validation has been added to **PutBucketCors** for `CORSRule.AllowedOrigins`. The `AllowedOrigins` list can no longer be empty—otherwise a **MalformedXML** error is returned. Additionally, each origin is now validated to ensure it does not contain more than one wildcard.

A similar validation has been added for `AllowedMethods`. The list must not be empty, or a **MalformedXML** error is returned. Previously, empty method values (e.g., `[]string{""}`) were incorrectly treated as valid. This has been fixed, and an **UnsupportedCORSMethod** error is now returned.
2026-02-24 16:59:38 +04:00

274 lines
9.2 KiB
Go

// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package integration
import (
"fmt"
"net/http"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/versity/versitygw/s3err"
)
func PutBucketCors_non_existing_bucket(s *S3Conf) error {
testName := "PutBucketCors_non_existing_bucket"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
err := putBucketCors(s3client, &s3.PutBucketCorsInput{
Bucket: getPtr("non-existing-bucket"),
CORSConfiguration: &types.CORSConfiguration{
CORSRules: []types.CORSRule{
{
AllowedOrigins: []string{"http://origin.com"},
AllowedMethods: []string{http.MethodGet},
},
},
},
})
return checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucket))
})
}
func PutBucketCors_empty_cors_rules(s *S3Conf) error {
testName := "PutBucketCors_empty_cors_rules"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
err := putBucketCors(s3client, &s3.PutBucketCorsInput{
Bucket: &bucket,
CORSConfiguration: &types.CORSConfiguration{
CORSRules: []types.CORSRule{},
},
})
return checkApiErr(err, s3err.GetAPIError(s3err.ErrMalformedXML))
})
}
func PutBucketCors_invalid_allowed_origins(s *S3Conf) error {
testName := "PutBucketCors_invalid_allowed_origins"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
for i, test := range []struct {
allowedOrigins []string
expectedErr s3err.APIError
}{
// empty allowed origins
{[]string{}, s3err.GetAPIError(s3err.ErrMalformedXML)},
// multiple wildcards
{[]string{"*example.com*"}, s3err.GetMultipleWildcardCORSOriginErr("*example.com*")},
{[]string{"http://127.0.0.1:7070", "https://*:7070/*"}, s3err.GetMultipleWildcardCORSOriginErr("https://*:7070/*")},
{[]string{"a/really/*/long/*/path/*"}, s3err.GetMultipleWildcardCORSOriginErr("a/really/*/long/*/path/*")},
{[]string{"http://***", "www.example.net"}, s3err.GetMultipleWildcardCORSOriginErr("http://***")},
} {
err := putBucketCors(s3client, &s3.PutBucketCorsInput{
Bucket: &bucket,
CORSConfiguration: &types.CORSConfiguration{
CORSRules: []types.CORSRule{
{
AllowedOrigins: test.allowedOrigins,
AllowedMethods: []string{http.MethodPost},
AllowedHeaders: []string{"x-amz-expected-bucket-owner"},
ExposeHeaders: []string{"x-vgw-something"},
},
},
},
})
if err := checkApiErr(err, test.expectedErr); err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
}
return nil
})
}
func PutBucketCors_invalid_method(s *S3Conf) error {
testName := "PutBucketCors_invalid_method"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
for _, test := range []struct {
allowedMethods []string
expectedErr s3err.APIError
}{
// empty array
{[]string{}, s3err.GetAPIError(s3err.ErrMalformedXML)},
// empty method
{[]string{""}, s3err.GetUnsopportedCORSMethodErr("")},
// invalid methods
{[]string{"get"}, s3err.GetUnsopportedCORSMethodErr("get")},
{[]string{"put"}, s3err.GetUnsopportedCORSMethodErr("put")},
{[]string{"post"}, s3err.GetUnsopportedCORSMethodErr("post")},
{[]string{"head"}, s3err.GetUnsopportedCORSMethodErr("head")},
{[]string{"delete"}, s3err.GetUnsopportedCORSMethodErr("delete")},
{[]string{http.MethodGet, http.MethodPatch}, s3err.GetUnsopportedCORSMethodErr(http.MethodPatch)},
{[]string{http.MethodPost, http.MethodOptions}, s3err.GetUnsopportedCORSMethodErr(http.MethodOptions)},
{[]string{http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut, http.MethodDelete, "invalid_method"}, s3err.GetUnsopportedCORSMethodErr("invalid_method")},
} {
err := putBucketCors(s3client, &s3.PutBucketCorsInput{
Bucket: &bucket,
CORSConfiguration: &types.CORSConfiguration{
CORSRules: []types.CORSRule{
{
AllowedOrigins: []string{"http://origin.com"},
AllowedMethods: test.allowedMethods,
AllowedHeaders: []string{"X-Amz-Date"},
ExposeHeaders: []string{"Authorization"},
},
},
},
})
if err := checkApiErr(err, test.expectedErr); err != nil {
return err
}
}
return nil
})
}
func PutBucketCors_invalid_header(s *S3Conf) error {
testName := "PutBucketCors_invalid_header"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
for _, test := range []struct {
invalidHeader string
headers []string
}{
{"invalid header", []string{"X-Amz-Date", "X-Amz-Content-Sha256", "invalid header"}},
{"X-Custom:Header", []string{"Authorization", "X-Custom:Header"}},
{"X(Custom)", []string{"Content-Length", "X(Custom)"}},
{"Bad/Header", []string{"Content-Encoding", "Bad/Header"}},
{"X[Key]", []string{"Date", "X[Key]"}},
{"Bad=Name", []string{"X-Amz-Custome-Header", "Bad=Name"}},
{`X"Quote"`, []string{`X"Quote"`}},
} {
// first check for allowed headers
cfg := &types.CORSConfiguration{
CORSRules: []types.CORSRule{
{
AllowedOrigins: []string{"http://origin.com"},
AllowedMethods: []string{http.MethodPost},
AllowedHeaders: test.headers,
ExposeHeaders: []string{"Authorization"},
},
},
}
err := putBucketCors(s3client, &s3.PutBucketCorsInput{
Bucket: &bucket,
CORSConfiguration: cfg,
})
if err := checkApiErr(err, s3err.GetInvalidCORSHeaderErr(test.invalidHeader)); err != nil {
return err
}
// second check for expose headers
cfg.CORSRules[0].AllowedHeaders = []string{"X-Amz-Date"} // set to any valid header
cfg.CORSRules[0].ExposeHeaders = test.headers
err = putBucketCors(s3client, &s3.PutBucketCorsInput{
Bucket: &bucket,
CORSConfiguration: cfg,
})
if err := checkApiErr(err, s3err.GetInvalidCORSHeaderErr(test.invalidHeader)); err != nil {
return err
}
}
return nil
})
}
func PutBucketCors_md5(s *S3Conf) error {
testName := "PutBucketCors_md5"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
cfg := &types.CORSConfiguration{
CORSRules: []types.CORSRule{
{
AllowedOrigins: []string{"http://origin.com", "something.net"},
AllowedMethods: []string{http.MethodPost, http.MethodPut, http.MethodHead},
AllowedHeaders: []string{"X-Amz-Date", "X-Amz-Meta-Something", "Content-Type"},
ExposeHeaders: []string{"Authorization", "Content-Disposition"},
MaxAgeSeconds: getPtr(int32(125)),
ID: getPtr("my-id"),
},
},
}
for i, test := range []struct {
md5 string
err error
}{
// invalid content-md5
{"invalid", s3err.GetAPIError(s3err.ErrInvalidDigest)},
// incorrect content-md5
{"uU0nuZNNPgilLlLX2n2r+s==", s3err.GetAPIError(s3err.ErrBadDigest)},
// correct content-md5
{"liZChnDYdpG46exsGGaBhg==", nil},
} {
err := putBucketCors(s3client, &s3.PutBucketCorsInput{
Bucket: &bucket,
CORSConfiguration: cfg,
ContentMD5: &test.md5,
})
if test.err == nil && err != nil {
return fmt.Errorf("test %v failed: expected no error but got %v", i+1, err)
}
if test.err != nil {
apiErr, ok := test.err.(s3err.APIError)
if !ok {
return fmt.Errorf("test %v failed: expected s3err.APIError", i+1)
}
if err := checkApiErr(err, apiErr); err != nil {
return fmt.Errorf("test %v failed: %v", i+1, err)
}
}
}
return nil
})
}
func PutBucketCors_success(s *S3Conf) error {
testName := "PutBucketCors_success"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
maxAgePositive, maxAgeNegative := int32(3000), int32(-100)
return putBucketCors(s3client, &s3.PutBucketCorsInput{
Bucket: &bucket,
CORSConfiguration: &types.CORSConfiguration{
CORSRules: []types.CORSRule{
{
AllowedOrigins: []string{"http://origin.com"},
AllowedMethods: []string{http.MethodPost, http.MethodPut},
AllowedHeaders: []string{"X-Amz-Date"},
ExposeHeaders: []string{"Authorization"},
// weirdely negative max age seconds are also considered valid
MaxAgeSeconds: &maxAgeNegative,
},
{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{http.MethodDelete, http.MethodGet, http.MethodHead},
AllowedHeaders: []string{"Content-Type", "Content-Encoding", "Content-MD5"},
ExposeHeaders: []string{"Authorization", "X-Amz-Date", "X-Amz-Conten-Sha256"},
ID: getPtr("id"),
MaxAgeSeconds: &maxAgePositive,
},
{
AllowedOrigins: []string{"http://example.com", "https://something.net", "http://*origin.com"},
AllowedMethods: []string{http.MethodGet},
},
},
},
})
})
}