mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 11:21:47 +00:00
apiserver: CVE-2022-1996, validate cors-allowed-origins server option
This commit is contained in:
parent
7b01daba71
commit
841311ada2
@ -68,6 +68,25 @@ func TestCORSAllowedOrigins(t *testing.T) {
|
|||||||
origins: []string{"http://example.com", "example.com"},
|
origins: []string{"http://example.com", "example.com"},
|
||||||
allowed: true,
|
allowed: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// CVE-2022-1996: regular expression 'example.com' matches
|
||||||
|
// 'example.com.hacker.org' because it does not pin to the start
|
||||||
|
// and end of the host.
|
||||||
|
// The CVE should not occur in a real kubernetes cluster since we
|
||||||
|
// validate the regular expression specified in --cors-allowed-origins
|
||||||
|
// to ensure that it pins to the start and end of the host name.
|
||||||
|
name: "regex does not pin, CVE-2022-1996 is not prevented",
|
||||||
|
allowedOrigins: []string{"example.com"},
|
||||||
|
origins: []string{"http://example.com.hacker.org", "http://example.com.hacker.org:8080"},
|
||||||
|
allowed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// with a proper regular expression we can prevent CVE-2022-1996
|
||||||
|
name: "regex pins to start/end of the host name, CVE-2022-1996 is prevented",
|
||||||
|
allowedOrigins: []string{`//example.com(:|$)`},
|
||||||
|
origins: []string{"http://example.com.hacker.org", "http://example.com.hacker.org:8080"},
|
||||||
|
allowed: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
|
@ -19,6 +19,7 @@ package options
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -31,6 +32,16 @@ import (
|
|||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
corsAllowedOriginsHelpText = "List of allowed origins for CORS, comma separated. " +
|
||||||
|
"An allowed origin can be a regular expression to support subdomain matching. " +
|
||||||
|
"If this list is empty CORS will not be enabled. " +
|
||||||
|
"Please ensure each expression matches the entire hostname by anchoring " +
|
||||||
|
"to the start with '^' or including the '//' prefix, and by anchoring to the " +
|
||||||
|
"end with '$' or including the ':' port separator suffix. " +
|
||||||
|
"Examples of valid expressions are '//example\\.com(:|$)' and '^https://example\\.com(:|$)'"
|
||||||
|
)
|
||||||
|
|
||||||
// ServerRunOptions contains the options while running a generic api server.
|
// ServerRunOptions contains the options while running a generic api server.
|
||||||
type ServerRunOptions struct {
|
type ServerRunOptions struct {
|
||||||
AdvertiseAddress net.IP
|
AdvertiseAddress net.IP
|
||||||
@ -161,6 +172,10 @@ func (s *ServerRunOptions) Validate() []error {
|
|||||||
if err := validateHSTSDirectives(s.HSTSDirectives); err != nil {
|
if err := validateHSTSDirectives(s.HSTSDirectives); err != nil {
|
||||||
errors = append(errors, err)
|
errors = append(errors, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := validateCorsAllowedOriginList(s.CorsAllowedOriginList); err != nil {
|
||||||
|
errors = append(errors, err)
|
||||||
|
}
|
||||||
return errors
|
return errors
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,6 +198,57 @@ func validateHSTSDirectives(hstsDirectives []string) error {
|
|||||||
return errors.NewAggregate(allErrors)
|
return errors.NewAggregate(allErrors)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateCorsAllowedOriginList(corsAllowedOriginList []string) error {
|
||||||
|
allErrors := []error{}
|
||||||
|
validateRegexFn := func(regexpStr string) error {
|
||||||
|
if _, err := regexp.Compile(regexpStr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// the regular expression should pin to the start and end of the host
|
||||||
|
// in the origin header, this will prevent CVE-2022-1996.
|
||||||
|
// possible ways it can pin to the start of host in the origin header:
|
||||||
|
// - match the start of the origin with '^'
|
||||||
|
// - match what separates the scheme and host with '//' or '://',
|
||||||
|
// this pins to the start of host in the origin header.
|
||||||
|
// possible ways it can match the end of the host in the origin header:
|
||||||
|
// - match the end of the origin with '$'
|
||||||
|
// - with a capture group that matches the host and port separator '(:|$)'
|
||||||
|
// We will relax the validation to check if these regex markers
|
||||||
|
// are present in the user specified expression.
|
||||||
|
var pinStart, pinEnd bool
|
||||||
|
for _, prefix := range []string{"^", "//"} {
|
||||||
|
if strings.Contains(regexpStr, prefix) {
|
||||||
|
pinStart = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, suffix := range []string{"$", ":"} {
|
||||||
|
if strings.Contains(regexpStr, suffix) {
|
||||||
|
pinEnd = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !pinStart || !pinEnd {
|
||||||
|
return fmt.Errorf("regular expression does not pin to start/end of host in the origin header")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, regexp := range corsAllowedOriginList {
|
||||||
|
if len(regexp) == 0 {
|
||||||
|
allErrors = append(allErrors, fmt.Errorf("empty value in --cors-allowed-origins, help: %s", corsAllowedOriginsHelpText))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateRegexFn(regexp); err != nil {
|
||||||
|
err = fmt.Errorf("--cors-allowed-origins has an invalid regular expression: %v, help: %s", err, corsAllowedOriginsHelpText)
|
||||||
|
allErrors = append(allErrors, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.NewAggregate(allErrors)
|
||||||
|
}
|
||||||
|
|
||||||
// AddUniversalFlags adds flags for a specific APIServer to the specified FlagSet
|
// AddUniversalFlags adds flags for a specific APIServer to the specified FlagSet
|
||||||
func (s *ServerRunOptions) AddUniversalFlags(fs *pflag.FlagSet) {
|
func (s *ServerRunOptions) AddUniversalFlags(fs *pflag.FlagSet) {
|
||||||
// Note: the weird ""+ in below lines seems to be the only way to get gofmt to
|
// Note: the weird ""+ in below lines seems to be the only way to get gofmt to
|
||||||
@ -194,9 +260,7 @@ func (s *ServerRunOptions) AddUniversalFlags(fs *pflag.FlagSet) {
|
|||||||
"will be used. If --bind-address is unspecified, the host's default interface will "+
|
"will be used. If --bind-address is unspecified, the host's default interface will "+
|
||||||
"be used.")
|
"be used.")
|
||||||
|
|
||||||
fs.StringSliceVar(&s.CorsAllowedOriginList, "cors-allowed-origins", s.CorsAllowedOriginList, ""+
|
fs.StringSliceVar(&s.CorsAllowedOriginList, "cors-allowed-origins", s.CorsAllowedOriginList, corsAllowedOriginsHelpText)
|
||||||
"List of allowed origins for CORS, comma separated. An allowed origin can be a regular "+
|
|
||||||
"expression to support subdomain matching. If this list is empty CORS will not be enabled.")
|
|
||||||
|
|
||||||
fs.StringSliceVar(&s.HSTSDirectives, "strict-transport-security-directives", s.HSTSDirectives, ""+
|
fs.StringSliceVar(&s.HSTSDirectives, "strict-transport-security-directives", s.HSTSDirectives, ""+
|
||||||
"List of directives for HSTS, comma separated. If this list is empty, then HSTS directives will not "+
|
"List of directives for HSTS, comma separated. If this list is empty, then HSTS directives will not "+
|
||||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package options
|
package options
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -164,7 +165,7 @@ func TestServerRunOptionsValidate(t *testing.T) {
|
|||||||
name: "Test when ServerRunOptions is valid",
|
name: "Test when ServerRunOptions is valid",
|
||||||
testOptions: &ServerRunOptions{
|
testOptions: &ServerRunOptions{
|
||||||
AdvertiseAddress: netutils.ParseIPSloppy("192.168.10.10"),
|
AdvertiseAddress: netutils.ParseIPSloppy("192.168.10.10"),
|
||||||
CorsAllowedOriginList: []string{"10.10.10.100", "10.10.10.200"},
|
CorsAllowedOriginList: []string{"^10.10.10.100$", "^10.10.10.200$"},
|
||||||
HSTSDirectives: []string{"max-age=31536000", "includeSubDomains", "preload"},
|
HSTSDirectives: []string{"max-age=31536000", "includeSubDomains", "preload"},
|
||||||
MaxRequestsInFlight: 400,
|
MaxRequestsInFlight: 400,
|
||||||
MaxMutatingRequestsInFlight: 200,
|
MaxMutatingRequestsInFlight: 200,
|
||||||
@ -189,3 +190,74 @@ func TestServerRunOptionsValidate(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateCorsAllowedOriginList(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
regexp [][]string
|
||||||
|
errShouldContain string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
regexp: [][]string{
|
||||||
|
{}, // empty list, the cluster operator wants to disable CORS
|
||||||
|
{`^http://foo.com$`},
|
||||||
|
{`^http://foo.com`}, // valid, because we relaxed the validation
|
||||||
|
{`://foo.com$`},
|
||||||
|
{`//foo.com$`},
|
||||||
|
{`^http://foo.com(:|$)`},
|
||||||
|
{`://foo.com(:|$)`},
|
||||||
|
{`//foo.com(:|$)`},
|
||||||
|
{`(^foo.com$)`},
|
||||||
|
{`^http://foo.com$`, `//bar.com(:|$)`},
|
||||||
|
},
|
||||||
|
errShouldContain: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// empty string, indicates that the cluster operator
|
||||||
|
// specified --cors-allowed-origins=""
|
||||||
|
regexp: [][]string{
|
||||||
|
{`^http://foo.com$`, ``},
|
||||||
|
},
|
||||||
|
errShouldContain: "empty value in --cors-allowed-origins",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regexp: [][]string{
|
||||||
|
{`^foo.com`},
|
||||||
|
{`//foo.com`},
|
||||||
|
{`foo.com$`},
|
||||||
|
{`foo.com(:|$)`},
|
||||||
|
},
|
||||||
|
errShouldContain: "regular expression does not pin to start/end of host in the origin header",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regexp: [][]string{
|
||||||
|
{`^http://foo.com$`, `^foo.com`}, // one good followed by a bad one
|
||||||
|
},
|
||||||
|
errShouldContain: "regular expression does not pin to start/end of host in the origin header",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
for _, regexp := range test.regexp {
|
||||||
|
t.Run(fmt.Sprintf("regexp/%s", regexp), func(t *testing.T) {
|
||||||
|
options := NewServerRunOptions()
|
||||||
|
if errs := options.Validate(); len(errs) != 0 {
|
||||||
|
t.Fatalf("wrong test setup: %#v", errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
options.CorsAllowedOriginList = regexp
|
||||||
|
errsGot := options.Validate()
|
||||||
|
switch {
|
||||||
|
case len(test.errShouldContain) == 0:
|
||||||
|
if len(errsGot) != 0 {
|
||||||
|
t.Errorf("expected no error, but got: %v", errsGot)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if len(errsGot) == 0 ||
|
||||||
|
!strings.Contains(utilerrors.NewAggregate(errsGot).Error(), test.errShouldContain) {
|
||||||
|
t.Errorf("expected error to contain: %s, but got: %v", test.errShouldContain, errsGot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user