[#672] Support wildcard in allowed origins and headers

Signed-off-by: Marina Biryukova <m.biryukova@yadro.com>
This commit is contained in:
Marina Biryukova 2025-03-31 14:42:59 +03:00
parent 2ad2531d3a
commit e45c1a2188
6 changed files with 724 additions and 18 deletions

View file

@ -2,6 +2,8 @@ package handler
import (
"net/http"
"regexp"
"slices"
"strconv"
"strings"
@ -110,6 +112,10 @@ func (h *handler) AppendCORSHeaders(w http.ResponseWriter, r *http.Request) {
if origin == "" {
return
}
method := r.Header.Get(api.AccessControlRequestMethod)
if method == "" {
method = r.Method
}
ctx = qostagging.ContextWithIOTag(ctx, util.InternalIOTag)
reqInfo := middleware.GetReqInfo(ctx)
@ -132,9 +138,9 @@ func (h *handler) AppendCORSHeaders(w http.ResponseWriter, r *http.Request) {
for _, rule := range cors.CORSRules {
for _, o := range rule.AllowedOrigins {
if o == origin {
if o == origin || (strings.Contains(o, "*") && len(o) > 1 && match(o, origin)) {
for _, m := range rule.AllowedMethods {
if m == r.Method {
if m == method {
w.Header().Set(api.AccessControlAllowOrigin, origin)
w.Header().Set(api.AccessControlAllowMethods, strings.Join(rule.AllowedMethods, ", "))
w.Header().Set(api.AccessControlAllowCredentials, "true")
@ -145,7 +151,7 @@ func (h *handler) AppendCORSHeaders(w http.ResponseWriter, r *http.Request) {
}
if o == wildcard {
for _, m := range rule.AllowedMethods {
if m == r.Method {
if m == method {
if withCredentials {
w.Header().Set(api.AccessControlAllowOrigin, origin)
w.Header().Set(api.AccessControlAllowCredentials, "true")
@ -199,7 +205,7 @@ func (h *handler) Preflight(w http.ResponseWriter, r *http.Request) {
for _, rule := range cors.CORSRules {
for _, o := range rule.AllowedOrigins {
if o == origin || o == wildcard {
if o == origin || o == wildcard || (strings.Contains(o, "*") && match(o, origin)) {
for _, m := range rule.AllowedMethods {
if m == method {
if !checkSubslice(rule.AllowedHeaders, headers) {
@ -235,12 +241,9 @@ func (h *handler) Preflight(w http.ResponseWriter, r *http.Request) {
}
func checkSubslice(slice []string, subSlice []string) bool {
if sliceContains(slice, wildcard) {
if slices.Contains(slice, wildcard) {
return true
}
if len(subSlice) > len(slice) {
return false
}
for _, r := range subSlice {
if !sliceContains(slice, r) {
return false
@ -251,9 +254,16 @@ func checkSubslice(slice []string, subSlice []string) bool {
func sliceContains(slice []string, str string) bool {
for _, s := range slice {
if s == str {
if s == str || (strings.Contains(s, "*") && match(s, str)) {
return true
}
}
return false
}
func match(tmpl, str string) bool {
regexpStr := "^" + regexp.QuoteMeta(tmpl) + "$"
regexpStr = regexpStr[:strings.Index(regexpStr, "*")-1] + "." + regexpStr[strings.Index(regexpStr, "*"):]
reg := regexp.MustCompile(regexpStr)
return reg.Match([]byte(str))
}

View file

@ -63,8 +63,8 @@ func TestPreflight(t *testing.T) {
<AllowedMethod>GET</AllowedMethod>
<AllowedOrigin>http://www.example.com</AllowedOrigin>
<AllowedHeader>Authorization</AllowedHeader>
<ExposeHeader>x-amz-*</ExposeHeader>
<ExposeHeader>X-Amz-*</ExposeHeader>
<ExposeHeader>x-amz-request-id</ExposeHeader>
<ExposeHeader>X-Amz-Request-Id</ExposeHeader>
<MaxAgeSeconds>600</MaxAgeSeconds>
</CORSRule>
</CORSConfiguration>
@ -138,7 +138,7 @@ func TestPreflight(t *testing.T) {
require.Equal(t, tc.origin, w.Header().Get(api.AccessControlAllowOrigin))
require.Equal(t, tc.method, w.Header().Get(api.AccessControlAllowMethods))
require.Equal(t, tc.headers, w.Header().Get(api.AccessControlAllowHeaders))
require.Equal(t, "x-amz-*, X-Amz-*", w.Header().Get(api.AccessControlExposeHeaders))
require.Equal(t, "x-amz-request-id, X-Amz-Request-Id", w.Header().Get(api.AccessControlExposeHeaders))
require.Equal(t, "true", w.Header().Get(api.AccessControlAllowCredentials))
require.Equal(t, "600", w.Header().Get(api.AccessControlMaxAge))
}
@ -230,6 +230,109 @@ func TestPreflightWildcardOrigin(t *testing.T) {
}
}
func TestAppendCORSHeadersWildcardOrigin(t *testing.T) {
body := `
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedOrigin>*</AllowedOrigin>
</CORSRule>
</CORSConfiguration>
`
hc := prepareHandlerContext(t)
bktName := "bucket-append-cors-headers-wildcard-test"
box, _ := createAccessBox(t)
w, r := prepareTestRequest(hc, bktName, "", nil)
ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
r = r.WithContext(ctx)
hc.Handler().CreateBucketHandler(w, r)
assertStatus(t, w, http.StatusOK)
putBucketCORS(hc, bktName, body)
for _, tc := range []struct {
name string
requestHeaders map[string]string
expectedHeaders map[string]string
}{
{
name: "Valid get",
requestHeaders: map[string]string{
api.Origin: "http://www.example.com",
api.AccessControlRequestMethod: "GET",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "*",
api.AccessControlAllowCredentials: "",
api.Vary: "",
api.AccessControlAllowMethods: "GET, PUT",
},
},
{
name: "Valid get with Authorization",
requestHeaders: map[string]string{
api.Origin: "http://www.example.com",
api.AccessControlRequestMethod: "GET",
api.Authorization: "value",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "http://www.example.com",
api.AccessControlAllowCredentials: "true",
api.Vary: api.Origin,
api.AccessControlAllowMethods: "GET, PUT",
},
},
{
name: "Empty origin",
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "",
api.AccessControlAllowCredentials: "",
api.Vary: "",
api.AccessControlAllowMethods: "",
},
},
{
name: "Empty request method",
requestHeaders: map[string]string{
api.Origin: "http://www.example.com",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "*",
api.AccessControlAllowCredentials: "",
api.Vary: "",
api.AccessControlAllowMethods: "GET, PUT",
},
},
{
name: "Not allowed method",
requestHeaders: map[string]string{
api.Origin: "http://www.example.com",
api.AccessControlRequestMethod: "DELETE",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "",
api.AccessControlAllowCredentials: "",
api.Vary: "",
api.AccessControlAllowMethods: "",
},
},
} {
t.Run(tc.name, func(t *testing.T) {
w, r = prepareTestPayloadRequest(hc, bktName, "", nil)
for k, v := range tc.requestHeaders {
r.Header.Set(k, v)
}
hc.Handler().AppendCORSHeaders(w, r)
for k, v := range tc.expectedHeaders {
require.Equal(t, v, w.Header().Get(k))
}
})
}
}
func TestGetLatestCORSVersion(t *testing.T) {
bodyTree := `
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
@ -346,6 +449,509 @@ func TestDeleteCORSInDeleteBucket(t *testing.T) {
require.Len(t, hc.tp.Objects(), 1) // CORS object in bucket container is not deleted
}
func TestAllowedOriginWildcards(t *testing.T) {
hc := prepareHandlerContext(t)
bktName := "bucket-allowed-origin-wildcards"
createBucket(hc, bktName)
cfg := &data.CORSConfiguration{
CORSRules: []data.CORSRule{
{
AllowedOrigins: []string{"*suffix.example"},
AllowedMethods: []string{"PUT"},
},
{
AllowedOrigins: []string{"https://*example"},
AllowedMethods: []string{"PUT"},
},
{
AllowedOrigins: []string{"prefix.example*"},
AllowedMethods: []string{"PUT"},
},
},
}
body, err := xml.Marshal(cfg)
require.NoError(t, err)
putBucketCORS(hc, bktName, string(body))
for _, tc := range []struct {
name string
handler func(w http.ResponseWriter, r *http.Request)
requestHeaders map[string]string
expectedHeaders map[string]string
expectedStatus int
}{
{
name: "append cors headers, empty request cors headers",
handler: hc.Handler().AppendCORSHeaders,
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "",
api.AccessControlAllowMethods: "",
},
},
{
name: "append cors headers, invalid origin",
handler: hc.Handler().AppendCORSHeaders,
requestHeaders: map[string]string{
api.Origin: "https://origin.com",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "",
api.AccessControlAllowMethods: "",
},
},
{
name: "append cors headers, first rule, no symbols in place of wildcard",
handler: hc.Handler().AppendCORSHeaders,
requestHeaders: map[string]string{
api.Origin: "suffix.example",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "suffix.example",
api.AccessControlAllowMethods: "PUT",
},
},
{
name: "append cors headers, first rule, valid origin",
handler: hc.Handler().AppendCORSHeaders,
requestHeaders: map[string]string{
api.Origin: "http://suffix.example",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "http://suffix.example",
api.AccessControlAllowMethods: "PUT",
},
},
{
name: "append cors headers, first rule, invalid origin",
handler: hc.Handler().AppendCORSHeaders,
requestHeaders: map[string]string{
api.Origin: "http://suffix-example",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "",
api.AccessControlAllowMethods: "",
},
},
{
name: "append cors headers, second rule, no symbols in place of wildcard",
handler: hc.Handler().AppendCORSHeaders,
requestHeaders: map[string]string{
api.Origin: "https://example",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "https://example",
api.AccessControlAllowMethods: "PUT",
},
},
{
name: "append cors headers, second rule, valid origin",
handler: hc.Handler().AppendCORSHeaders,
requestHeaders: map[string]string{
api.Origin: "https://www.example",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "https://www.example",
api.AccessControlAllowMethods: "PUT",
},
},
{
name: "append cors headers, second rule, invalid origin",
handler: hc.Handler().AppendCORSHeaders,
requestHeaders: map[string]string{
api.Origin: "https://www.example.com",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "",
api.AccessControlAllowMethods: "",
},
},
{
name: "append cors headers, third rule, no symbols in place of wildcard",
handler: hc.Handler().AppendCORSHeaders,
requestHeaders: map[string]string{
api.Origin: "prefix.example",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "prefix.example",
api.AccessControlAllowMethods: "PUT",
},
},
{
name: "append cors headers, third rule, valid origin",
handler: hc.Handler().AppendCORSHeaders,
requestHeaders: map[string]string{
api.Origin: "prefix.example.com",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "prefix.example.com",
api.AccessControlAllowMethods: "PUT",
},
},
{
name: "append cors headers, third rule, invalid origin",
handler: hc.Handler().AppendCORSHeaders,
requestHeaders: map[string]string{
api.Origin: "www.prefix.example",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "",
api.AccessControlAllowMethods: "",
},
},
{
name: "append cors headers, third rule, invalid request method in header",
handler: hc.Handler().AppendCORSHeaders,
requestHeaders: map[string]string{
api.Origin: "prefix.example.com",
api.AccessControlRequestMethod: "GET",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "",
api.AccessControlAllowMethods: "",
},
},
{
name: "append cors headers, third rule, valid request method in header",
handler: hc.Handler().AppendCORSHeaders,
requestHeaders: map[string]string{
api.Origin: "prefix.example.com",
api.AccessControlRequestMethod: "PUT",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "prefix.example.com",
api.AccessControlAllowMethods: "PUT",
},
},
{
name: "preflight, empty request cors headers",
handler: hc.Handler().Preflight,
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "",
api.AccessControlAllowMethods: "",
},
expectedStatus: http.StatusBadRequest,
},
{
name: "preflight, invalid origin",
handler: hc.Handler().Preflight,
requestHeaders: map[string]string{
api.Origin: "https://origin.com",
api.AccessControlRequestMethod: "PUT",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "",
api.AccessControlAllowMethods: "",
},
expectedStatus: http.StatusForbidden,
},
{
name: "preflight, first rule, no symbols in place of wildcard",
handler: hc.Handler().Preflight,
requestHeaders: map[string]string{
api.Origin: "suffix.example",
api.AccessControlRequestMethod: "PUT",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "suffix.example",
api.AccessControlAllowMethods: "PUT",
},
},
{
name: "prelight, first rule, valid origin",
handler: hc.Handler().Preflight,
requestHeaders: map[string]string{
api.Origin: "http://suffix.example",
api.AccessControlRequestMethod: "PUT",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "http://suffix.example",
api.AccessControlAllowMethods: "PUT",
},
},
{
name: "preflight, first rule, invalid origin",
handler: hc.Handler().Preflight,
requestHeaders: map[string]string{
api.Origin: "http://suffix-example",
api.AccessControlRequestMethod: "PUT",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "",
api.AccessControlAllowMethods: "",
},
expectedStatus: http.StatusForbidden,
},
{
name: "preflight, second rule, no symbols in place of wildcard",
handler: hc.Handler().Preflight,
requestHeaders: map[string]string{
api.Origin: "https://example",
api.AccessControlRequestMethod: "PUT",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "https://example",
api.AccessControlAllowMethods: "PUT",
},
},
{
name: "preflight, second rule, valid origin",
handler: hc.Handler().Preflight,
requestHeaders: map[string]string{
api.Origin: "https://www.example",
api.AccessControlRequestMethod: "PUT",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "https://www.example",
api.AccessControlAllowMethods: "PUT",
},
},
{
name: "preflight, second rule, invalid origin",
handler: hc.Handler().Preflight,
requestHeaders: map[string]string{
api.Origin: "https://www.example.com",
api.AccessControlRequestMethod: "PUT",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "",
api.AccessControlAllowMethods: "",
},
expectedStatus: http.StatusForbidden,
},
{
name: "preflight, third rule, no symbols in place of wildcard",
handler: hc.Handler().Preflight,
requestHeaders: map[string]string{
api.Origin: "prefix.example",
api.AccessControlRequestMethod: "PUT",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "prefix.example",
api.AccessControlAllowMethods: "PUT",
},
},
{
name: "preflight, third rule, valid origin",
handler: hc.Handler().Preflight,
requestHeaders: map[string]string{
api.Origin: "prefix.example.com",
api.AccessControlRequestMethod: "PUT",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "prefix.example.com",
api.AccessControlAllowMethods: "PUT",
},
},
{
name: "preflight, third rule, invalid origin",
handler: hc.Handler().Preflight,
requestHeaders: map[string]string{
api.Origin: "www.prefix.example",
api.AccessControlRequestMethod: "PUT",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "",
api.AccessControlAllowMethods: "",
},
expectedStatus: http.StatusForbidden,
},
{
name: "preflight, third rule, invalid request method in header",
handler: hc.Handler().Preflight,
requestHeaders: map[string]string{
api.Origin: "prefix.example.com",
api.AccessControlRequestMethod: "GET",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "",
api.AccessControlAllowMethods: "",
},
expectedStatus: http.StatusForbidden,
},
{
name: "preflight, third rule, valid request method in header",
handler: hc.Handler().Preflight,
requestHeaders: map[string]string{
api.Origin: "prefix.example.com",
api.AccessControlRequestMethod: "PUT",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "prefix.example.com",
api.AccessControlAllowMethods: "PUT",
},
},
} {
t.Run(tc.name, func(t *testing.T) {
w, r := prepareTestRequest(hc, bktName, "", nil)
for k, v := range tc.requestHeaders {
r.Header.Set(k, v)
}
tc.handler(w, r)
expectedStatus := http.StatusOK
if tc.expectedStatus != 0 {
expectedStatus = tc.expectedStatus
}
require.Equal(t, expectedStatus, w.Code)
for k, v := range tc.expectedHeaders {
require.Equal(t, v, w.Header().Get(k))
}
})
}
}
func TestAllowedHeaderWildcards(t *testing.T) {
hc := prepareHandlerContext(t)
bktName := "bucket-allowed-header-wildcards"
createBucket(hc, bktName)
cfg := &data.CORSConfiguration{
CORSRules: []data.CORSRule{
{
AllowedOrigins: []string{"https://www.example.com"},
AllowedMethods: []string{"HEAD"},
AllowedHeaders: []string{"*-suffix"},
},
{
AllowedOrigins: []string{"https://www.example.com"},
AllowedMethods: []string{"HEAD"},
AllowedHeaders: []string{"start-*-end"},
},
{
AllowedOrigins: []string{"https://www.example.com"},
AllowedMethods: []string{"HEAD"},
AllowedHeaders: []string{"X-Amz-*"},
},
},
}
body, err := xml.Marshal(cfg)
require.NoError(t, err)
putBucketCORS(hc, bktName, string(body))
for _, tc := range []struct {
name string
requestHeaders map[string]string
expectedHeaders map[string]string
expectedStatus int
}{
{
name: "first rule, valid headers",
requestHeaders: map[string]string{
api.Origin: "https://www.example.com",
api.AccessControlRequestMethod: "HEAD",
api.AccessControlRequestHeaders: "header-suffix, -suffix",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "https://www.example.com",
api.AccessControlAllowMethods: "HEAD",
api.AccessControlAllowHeaders: "header-suffix, -suffix",
},
},
{
name: "first rule, invalid headers",
requestHeaders: map[string]string{
api.Origin: "https://www.example.com",
api.AccessControlRequestMethod: "HEAD",
api.AccessControlRequestHeaders: "header-suffix-*",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "",
api.AccessControlAllowMethods: "",
api.AccessControlAllowHeaders: "",
},
expectedStatus: http.StatusForbidden,
},
{
name: "second rule, valid headers",
requestHeaders: map[string]string{
api.Origin: "https://www.example.com",
api.AccessControlRequestMethod: "HEAD",
api.AccessControlRequestHeaders: "start--end, start-header-end",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "https://www.example.com",
api.AccessControlAllowMethods: "HEAD",
api.AccessControlAllowHeaders: "start--end, start-header-end",
},
},
{
name: "second rule, invalid header ending",
requestHeaders: map[string]string{
api.Origin: "https://www.example.com",
api.AccessControlRequestMethod: "HEAD",
api.AccessControlRequestHeaders: "start-header-end-*",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "",
api.AccessControlAllowMethods: "",
api.AccessControlAllowHeaders: "",
},
expectedStatus: http.StatusForbidden,
},
{
name: "second rule, invalid header beginning",
requestHeaders: map[string]string{
api.Origin: "https://www.example.com",
api.AccessControlRequestMethod: "HEAD",
api.AccessControlRequestHeaders: "*-start-header-end",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "",
api.AccessControlAllowMethods: "",
api.AccessControlAllowHeaders: "",
},
expectedStatus: http.StatusForbidden,
},
{
name: "third rule, valid headers",
requestHeaders: map[string]string{
api.Origin: "https://www.example.com",
api.AccessControlRequestMethod: "HEAD",
api.AccessControlRequestHeaders: "X-Amz-Date, X-Amz-Content-Sha256",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "https://www.example.com",
api.AccessControlAllowMethods: "HEAD",
api.AccessControlAllowHeaders: "X-Amz-Date, X-Amz-Content-Sha256",
},
},
{
name: "third rule, invalid headers",
requestHeaders: map[string]string{
api.Origin: "https://www.example.com",
api.AccessControlRequestMethod: "HEAD",
api.AccessControlRequestHeaders: "Authorization",
},
expectedHeaders: map[string]string{
api.AccessControlAllowOrigin: "",
api.AccessControlAllowMethods: "",
api.AccessControlAllowHeaders: "",
},
expectedStatus: http.StatusForbidden,
},
} {
t.Run(tc.name, func(t *testing.T) {
w, r := prepareTestRequest(hc, bktName, "", nil)
for k, v := range tc.requestHeaders {
r.Header.Set(k, v)
}
hc.Handler().Preflight(w, r)
expectedStatus := http.StatusOK
if tc.expectedStatus != 0 {
expectedStatus = tc.expectedStatus
}
require.Equal(t, expectedStatus, w.Code)
for k, v := range tc.expectedHeaders {
require.Equal(t, v, w.Header().Get(k))
}
})
}
}
func addCORSToTree(hc *handlerContext, cors string, bkt *data.BucketInfo, corsCnrID cid.ID) {
var addr oid.Address
addr.SetContainer(corsCnrID)