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:
Ricardo Katz 2023-07-22 00:32:07 -03:00 committed by GitHub
parent 86c00a2310
commit c5f348ea2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
109 changed files with 4320 additions and 586 deletions

View file

@ -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)
}

View file

@ -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)