mirror of
https://github.com/project-zot/zot.git
synced 2026-06-18 05:28:07 +08:00
bfc59ad120
fix(security): suppress Allow-Credentials on wildcard CORS origin (CORS-1) Per CORS spec §3.2, Access-Control-Allow-Credentials must not be "true" when Access-Control-Allow-Origin is the wildcard "*". ACHeadersMiddleware (pkg/common/http_server.go) and getUIHeadersHandler (pkg/api/routes.go) now only emit the credentials header when an explicit, non-empty AllowOrigin is configured. Deployments that leave AllowOrigin blank (default wildcard) no longer produce a contradictory header pair. Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>
161 lines
4.6 KiB
Go
161 lines
4.6 KiB
Go
package common
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
jsoniter "github.com/json-iterator/go"
|
|
|
|
"zotregistry.dev/zot/v2/pkg/api/config"
|
|
"zotregistry.dev/zot/v2/pkg/api/constants"
|
|
apiErr "zotregistry.dev/zot/v2/pkg/api/errors"
|
|
reqCtx "zotregistry.dev/zot/v2/pkg/requestcontext"
|
|
)
|
|
|
|
func AllowedMethods(methods ...string) []string {
|
|
return append(methods, http.MethodOptions)
|
|
}
|
|
|
|
func AddExtensionSecurityHeaders() mux.MiddlewareFunc { //nolint:varnamelen
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
|
resp.Header().Set("X-Content-Type-Options", "nosniff")
|
|
|
|
next.ServeHTTP(resp, req)
|
|
})
|
|
}
|
|
}
|
|
|
|
func ACHeadersMiddleware(config *config.Config, allowedMethods ...string) mux.MiddlewareFunc {
|
|
allowedMethodsValue := strings.Join(allowedMethods, ",")
|
|
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
|
resp.Header().Set("Access-Control-Allow-Methods", allowedMethodsValue)
|
|
resp.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type,"+constants.SessionClientHeaderName)
|
|
|
|
// Access-Control-Allow-Credentials must not be "true" when
|
|
// Access-Control-Allow-Origin is the wildcard "*" (CORS spec §3.2).
|
|
// Only advertise credentials support when an explicit origin is set.
|
|
authConfig := config.CopyAuthConfig()
|
|
allowOrigin := strings.TrimSpace(config.GetAllowOrigin())
|
|
if authConfig.IsBasicAuthnEnabled() && allowOrigin != "" && allowOrigin != "*" {
|
|
resp.Header().Set("Access-Control-Allow-Credentials", "true")
|
|
}
|
|
|
|
if req.Method == http.MethodOptions {
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(resp, req)
|
|
})
|
|
}
|
|
}
|
|
|
|
func CORSHeadersMiddleware(allowOrigin string) mux.MiddlewareFunc {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
|
AddCORSHeaders(allowOrigin, response)
|
|
|
|
next.ServeHTTP(response, request)
|
|
})
|
|
}
|
|
}
|
|
|
|
func AddCORSHeaders(allowOrigin string, response http.ResponseWriter) {
|
|
if allowOrigin == "" {
|
|
response.Header().Set("Access-Control-Allow-Origin", "*")
|
|
} else {
|
|
response.Header().Set("Access-Control-Allow-Origin", allowOrigin)
|
|
}
|
|
}
|
|
|
|
// AuthzOnlyAdminsMiddleware permits only admin user access if auth is enabled.
|
|
func AuthzOnlyAdminsMiddleware(conf *config.Config) mux.MiddlewareFunc {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
|
// Get auth config safely
|
|
authConfig := conf.CopyAuthConfig()
|
|
if !authConfig.IsBasicAuthnEnabled() {
|
|
next.ServeHTTP(response, request)
|
|
|
|
return
|
|
}
|
|
|
|
realm := conf.GetRealm()
|
|
failDelay := authConfig.GetFailDelay()
|
|
|
|
// get userAccessControl built in previous authn/authz middlewares
|
|
userAc, err := reqCtx.UserAcFromContext(request.Context())
|
|
if err != nil { // should not happen as this has been previously checked for errors
|
|
AuthzFail(response, request, userAc.GetUsername(), realm, failDelay)
|
|
|
|
return
|
|
}
|
|
|
|
// reject non-admin access if authentication is enabled
|
|
if userAc != nil && !userAc.IsAdmin() {
|
|
AuthzFail(response, request, userAc.GetUsername(), realm, failDelay)
|
|
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(response, request)
|
|
})
|
|
}
|
|
}
|
|
|
|
func AuthzFail(w http.ResponseWriter, r *http.Request, identity, realm string, delay int) {
|
|
time.Sleep(time.Duration(delay) * time.Second)
|
|
|
|
// don't send auth headers if request is coming from UI
|
|
if r.Header.Get(constants.SessionClientHeaderName) != constants.SessionClientHeaderValue {
|
|
if realm == "" {
|
|
realm = "Authorization Required"
|
|
}
|
|
|
|
realm = "Basic realm=" + strconv.Quote(realm)
|
|
|
|
w.Header().Set("WWW-Authenticate", realm)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if identity == "" {
|
|
WriteJSON(w, http.StatusUnauthorized, apiErr.NewErrorList(apiErr.NewError(apiErr.UNAUTHORIZED)))
|
|
} else {
|
|
WriteJSON(w, http.StatusForbidden, apiErr.NewErrorList(apiErr.NewError(apiErr.DENIED)))
|
|
}
|
|
}
|
|
|
|
func WriteJSON(response http.ResponseWriter, status int, data any) {
|
|
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
|
|
|
body, err := json.Marshal(data)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
WriteData(response, status, constants.DefaultMediaType, body)
|
|
}
|
|
|
|
func WriteData(w http.ResponseWriter, status int, mediaType string, data []byte) {
|
|
w.Header().Set("Content-Type", mediaType)
|
|
w.WriteHeader(status)
|
|
_, _ = w.Write(data)
|
|
}
|
|
|
|
func QueryHasParams(values url.Values, params []string) bool {
|
|
for _, param := range params {
|
|
if !values.Has(param) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|