Implement annotation validation (#9673)
* Add validation to all annotations * Add annotation validation for fcgi * Fix reviews and fcgi e2e * Add flag to disable cross namespace validation * Add risk, flag for validation, tests * Add missing formating * Enable validation by default on tests * Test validation flag * remove ajp from list * Finalize validation changes * Add validations to CI * Update helm docs * Fix code review * Use a better name for annotation risk
This commit is contained in:
parent
86c00a2310
commit
c5f348ea2e
109 changed files with 4320 additions and 586 deletions
|
|
@ -24,6 +24,7 @@ import (
|
|||
"k8s.io/klog/v2"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
|
|
@ -38,20 +39,87 @@ var (
|
|||
// Regex are defined here to prevent information leak, if user tries to set anything not valid
|
||||
// that could cause the Response to contain some internal value/variable (like returning $pid, $upstream_addr, etc)
|
||||
// Origin must contain a http/s Origin (including or not the port) or the value '*'
|
||||
// This Regex is composed of the following:
|
||||
// * Sets a group that can be (https?://)?*?.something.com:port?
|
||||
// * Allows this to be repeated as much as possible, and separated by comma
|
||||
// Otherwise it should be '*'
|
||||
corsOriginRegexValidator = regexp.MustCompile(`^((((https?://)?(\*\.)?[A-Za-z0-9\-\.]*(:[0-9]+)?,?)+)|\*)?$`)
|
||||
// corsOriginRegex defines the regex for validation inside Parse
|
||||
corsOriginRegex = regexp.MustCompile(`^(https?://(\*\.)?[A-Za-z0-9\-\.]*(:[0-9]+)?|\*)?$`)
|
||||
// Method must contain valid methods list (PUT, GET, POST, BLA)
|
||||
// May contain or not spaces between each verb
|
||||
corsMethodsRegex = regexp.MustCompile(`^([A-Za-z]+,?\s?)+$`)
|
||||
// Headers must contain valid values only (X-HEADER12, X-ABC)
|
||||
// May contain or not spaces between each Header
|
||||
corsHeadersRegex = regexp.MustCompile(`^([A-Za-z0-9\-\_]+,?\s?)+$`)
|
||||
// Expose Headers must contain valid values only (*, X-HEADER12, X-ABC)
|
||||
// May contain or not spaces between each Header
|
||||
corsExposeHeadersRegex = regexp.MustCompile(`^(([A-Za-z0-9\-\_]+|\*),?\s?)+$`)
|
||||
)
|
||||
|
||||
const (
|
||||
corsEnableAnnotation = "enable-cors"
|
||||
corsAllowOriginAnnotation = "cors-allow-origin"
|
||||
corsAllowHeadersAnnotation = "cors-allow-headers"
|
||||
corsAllowMethodsAnnotation = "cors-allow-methods"
|
||||
corsAllowCredentialsAnnotation = "cors-allow-credentials" //#nosec G101
|
||||
corsExposeHeadersAnnotation = "cors-expose-headers"
|
||||
corsMaxAgeAnnotation = "cors-max-age"
|
||||
)
|
||||
|
||||
var corsAnnotation = parser.Annotation{
|
||||
Group: "cors",
|
||||
Annotations: parser.AnnotationFields{
|
||||
corsEnableAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables Cross-Origin Resource Sharing (CORS) in an Ingress rule`,
|
||||
},
|
||||
corsAllowOriginAnnotation: {
|
||||
Validator: parser.ValidateRegex(*corsOriginRegexValidator, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation controls what's the accepted Origin for CORS.
|
||||
This is a multi-valued field, separated by ','. It must follow this format: http(s)://origin-site.com or http(s)://origin-site.com:port
|
||||
It also supports single level wildcard subdomains and follows this format: http(s)://*.foo.bar, http(s)://*.bar.foo:8080 or http(s)://*.abc.bar.foo:9000`,
|
||||
},
|
||||
corsAllowHeadersAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.HeadersVariable, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation controls which headers are accepted.
|
||||
This is a multi-valued field, separated by ',' and accepts letters, numbers, _ and -`,
|
||||
},
|
||||
corsAllowMethodsAnnotation: {
|
||||
Validator: parser.ValidateRegex(*corsMethodsRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation controls which methods are accepted.
|
||||
This is a multi-valued field, separated by ',' and accepts only letters (upper and lower case)`,
|
||||
},
|
||||
corsAllowCredentialsAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation controls if credentials can be passed during CORS operations.`,
|
||||
},
|
||||
corsExposeHeadersAnnotation: {
|
||||
Validator: parser.ValidateRegex(*corsExposeHeadersRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation controls which headers are exposed to response.
|
||||
This is a multi-valued field, separated by ',' and accepts letters, numbers, _, - and *.`,
|
||||
},
|
||||
corsMaxAgeAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation controls how long, in seconds, preflight requests can be cached.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type cors struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// Config contains the Cors configuration to be used in the Ingress
|
||||
|
|
@ -67,7 +135,10 @@ type Config struct {
|
|||
|
||||
// NewParser creates a new CORS annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return cors{r}
|
||||
return cors{
|
||||
r: r,
|
||||
annotationConfig: corsAnnotation,
|
||||
}
|
||||
}
|
||||
|
||||
// Equal tests for equality between two External types
|
||||
|
|
@ -116,13 +187,16 @@ func (c cors) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
var err error
|
||||
config := &Config{}
|
||||
|
||||
config.CorsEnabled, err = parser.GetBoolAnnotation("enable-cors", ing)
|
||||
config.CorsEnabled, err = parser.GetBoolAnnotation(corsEnableAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("enable-cors is invalid, defaulting to 'false'")
|
||||
}
|
||||
config.CorsEnabled = false
|
||||
}
|
||||
|
||||
config.CorsAllowOrigin = []string{}
|
||||
unparsedOrigins, err := parser.GetStringAnnotation("cors-allow-origin", ing)
|
||||
unparsedOrigins, err := parser.GetStringAnnotation(corsAllowOriginAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err == nil {
|
||||
origins := strings.Split(unparsedOrigins, ",")
|
||||
for _, origin := range origins {
|
||||
|
|
@ -140,33 +214,53 @@ func (c cors) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
klog.Infof("Current config.corsAllowOrigin %v", config.CorsAllowOrigin)
|
||||
}
|
||||
} else {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("cors-allow-origin is invalid, defaulting to '*'")
|
||||
}
|
||||
config.CorsAllowOrigin = []string{"*"}
|
||||
}
|
||||
|
||||
config.CorsAllowHeaders, err = parser.GetStringAnnotation("cors-allow-headers", ing)
|
||||
if err != nil || !corsHeadersRegex.MatchString(config.CorsAllowHeaders) {
|
||||
config.CorsAllowHeaders, err = parser.GetStringAnnotation(corsAllowHeadersAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil || !parser.HeadersVariable.MatchString(config.CorsAllowHeaders) {
|
||||
config.CorsAllowHeaders = defaultCorsHeaders
|
||||
}
|
||||
|
||||
config.CorsAllowMethods, err = parser.GetStringAnnotation("cors-allow-methods", ing)
|
||||
config.CorsAllowMethods, err = parser.GetStringAnnotation(corsAllowMethodsAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil || !corsMethodsRegex.MatchString(config.CorsAllowMethods) {
|
||||
config.CorsAllowMethods = defaultCorsMethods
|
||||
}
|
||||
|
||||
config.CorsAllowCredentials, err = parser.GetBoolAnnotation("cors-allow-credentials", ing)
|
||||
config.CorsAllowCredentials, err = parser.GetBoolAnnotation(corsAllowCredentialsAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("cors-allow-credentials is invalid, defaulting to 'true'")
|
||||
}
|
||||
}
|
||||
config.CorsAllowCredentials = true
|
||||
}
|
||||
|
||||
config.CorsExposeHeaders, err = parser.GetStringAnnotation("cors-expose-headers", ing)
|
||||
config.CorsExposeHeaders, err = parser.GetStringAnnotation(corsExposeHeadersAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil || !corsExposeHeadersRegex.MatchString(config.CorsExposeHeaders) {
|
||||
config.CorsExposeHeaders = ""
|
||||
}
|
||||
|
||||
config.CorsMaxAge, err = parser.GetIntAnnotation("cors-max-age", ing)
|
||||
config.CorsMaxAge, err = parser.GetIntAnnotation(corsMaxAgeAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("cors-max-age is invalid, defaulting to %d", defaultCorsMaxAge)
|
||||
}
|
||||
config.CorsMaxAge = defaultCorsMaxAge
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (c cors) GetDocumentation() parser.AnnotationFields {
|
||||
return c.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a cors) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, corsAnnotation.Annotations)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,13 +75,13 @@ func TestIngressCorsConfigValid(t *testing.T) {
|
|||
data := map[string]string{}
|
||||
|
||||
// Valid
|
||||
data[parser.GetAnnotationWithPrefix("enable-cors")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-headers")] = "DNT,X-CustomHeader, Keep-Alive,User-Agent"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-credentials")] = "false"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-methods")] = "GET, PATCH"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-origin")] = "https://origin123.test.com:4443"
|
||||
data[parser.GetAnnotationWithPrefix("cors-expose-headers")] = "*, X-CustomResponseHeader"
|
||||
data[parser.GetAnnotationWithPrefix("cors-max-age")] = "600"
|
||||
data[parser.GetAnnotationWithPrefix(corsEnableAnnotation)] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowHeadersAnnotation)] = "DNT,X-CustomHeader, Keep-Alive,User-Agent"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowCredentialsAnnotation)] = "false"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowMethodsAnnotation)] = "GET, PATCH"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowOriginAnnotation)] = "https://origin123.test.com:4443"
|
||||
data[parser.GetAnnotationWithPrefix(corsExposeHeadersAnnotation)] = "*, X-CustomResponseHeader"
|
||||
data[parser.GetAnnotationWithPrefix(corsMaxAgeAnnotation)] = "600"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
corst, err := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
|
@ -95,31 +95,31 @@ func TestIngressCorsConfigValid(t *testing.T) {
|
|||
}
|
||||
|
||||
if !nginxCors.CorsEnabled {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("enable-cors")], nginxCors.CorsEnabled)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsEnableAnnotation)], nginxCors.CorsEnabled)
|
||||
}
|
||||
|
||||
if nginxCors.CorsAllowCredentials {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-credentials")], nginxCors.CorsAllowCredentials)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsAllowCredentialsAnnotation)], nginxCors.CorsAllowCredentials)
|
||||
}
|
||||
|
||||
if nginxCors.CorsAllowHeaders != "DNT,X-CustomHeader, Keep-Alive,User-Agent" {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-headers")], nginxCors.CorsAllowHeaders)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsAllowHeadersAnnotation)], nginxCors.CorsAllowHeaders)
|
||||
}
|
||||
|
||||
if nginxCors.CorsAllowMethods != "GET, PATCH" {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-methods")], nginxCors.CorsAllowMethods)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsAllowMethodsAnnotation)], nginxCors.CorsAllowMethods)
|
||||
}
|
||||
|
||||
if nginxCors.CorsAllowOrigin[0] != "https://origin123.test.com:4443" {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-origin")], nginxCors.CorsAllowOrigin)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsAllowOriginAnnotation)], nginxCors.CorsAllowOrigin)
|
||||
}
|
||||
|
||||
if nginxCors.CorsExposeHeaders != "*, X-CustomResponseHeader" {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-expose-headers")], nginxCors.CorsExposeHeaders)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsExposeHeadersAnnotation)], nginxCors.CorsExposeHeaders)
|
||||
}
|
||||
|
||||
if nginxCors.CorsMaxAge != 600 {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-max-age")], nginxCors.CorsMaxAge)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsMaxAgeAnnotation)], nginxCors.CorsMaxAge)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -129,13 +129,13 @@ func TestIngressCorsConfigInvalid(t *testing.T) {
|
|||
data := map[string]string{}
|
||||
|
||||
// Valid
|
||||
data[parser.GetAnnotationWithPrefix("enable-cors")] = "yes"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-headers")] = "@alright, #ingress"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-credentials")] = "no"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-methods")] = "GET, PATCH, $nginx"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-origin")] = "origin123.test.com:4443"
|
||||
data[parser.GetAnnotationWithPrefix("cors-expose-headers")] = "@alright, #ingress"
|
||||
data[parser.GetAnnotationWithPrefix("cors-max-age")] = "abcd"
|
||||
data[parser.GetAnnotationWithPrefix(corsEnableAnnotation)] = "yes"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowHeadersAnnotation)] = "@alright, #ingress"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowCredentialsAnnotation)] = "no"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowMethodsAnnotation)] = "GET, PATCH, $nginx"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowOriginAnnotation)] = "origin123.test.com:4443"
|
||||
data[parser.GetAnnotationWithPrefix(corsExposeHeadersAnnotation)] = "@alright, #ingress"
|
||||
data[parser.GetAnnotationWithPrefix(corsMaxAgeAnnotation)] = "abcd"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
corst, err := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue