Repository: kubernetes/ingress-nginx
Pull request #9511 (2023-01-15 23:29:34)

Implement pathType validation
## What this PR does / why we need it:
Ingress API states that pathType should validate the content and not allow non regular characters, unless the pathType is ImplementationSpecific.

This PR:
* Starts to validate all the paths on an ingress, checking for non standard characters - Accepted characters now are `A-Za-z0-9-._~/`
* The validation above is more wide if pathType is ImplementationSpecific, allowing also common regex characters like `^%$[](){}*+?`
* The set of characters is configurable in ConfigMap option `path-additional-allowed-chars`
* A user can disable this behavior setting the ConfigMap entry `disable-pathtype-validation` to true

**Disabling pathType validation will disable the uncommon characters on pathType Exact and Prefix, but the additional Allowed Characters will still be respected. If user wants to add more characters they should change the default**

## TODO
Implement documentation and helmchart configuration

## Types of changes
- [X] Bug fix (non-breaking change which fixes an issue)
- [X] New feature (non-breaking change which adds functionality)
- [ ] CVE Report (Scanner found CVE and adding report)
- [X] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation only

## Which issue/s this PR fixes
Part of release 1.6

## How Has This Been Tested?
Added a bunch of tests

## Does my pull request need a release note?

```release-note
BREAKING CHANGE - Ingress now validates the pathType and does not allow special characters on pathType Exact and Prefix
```


k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes/ingress-nginx/pull/9511#" title="Author self-approved">rikatz</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Fingress-nginx).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes/ingress-nginx/blob/main/OWNERS)~~ [rikatz]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
k8s-ci-robot: @rikatz: This issue is currently awaiting triage.

If Ingress contributors determines this is a relevant issue, they will accept it by applying the `triage/accepted` label and provide further guidance.

The `triage/accepted` label can be added by org members by writing `/triage accepted` in a comment.


<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
tao12345666333: /assign

Patch:
diff --git a/charts/ingress-nginx/README.md b/charts/ingress-nginx/README.md
index 65236e08c..174a8870d 100644
--- a/charts/ingress-nginx/README.md
+++ b/charts/ingress-nginx/README.md
@@ -311,6 +311,7 @@ Kubernetes: `>=1.20.0-0`
 | controller.containerPort | object | `{"http":80,"https":443}` | Configures the ports that the nginx-controller listens on |
 | controller.customTemplate.configMapKey | string | `""` |  |
 | controller.customTemplate.configMapName | string | `""` |  |
+| controller.disablePathTypeValidation | bool | `false` | This configuration defines if Ingress Controller should validate pathType. If this is true, special characters will be allowed on paths of any pathType. If false, special characters are only allowed on paths with pathType = ImplementationSpecific |
 | controller.dnsConfig | object | `{}` | Optionally customize the pod dnsConfig. |
 | controller.dnsPolicy | string | `"ClusterFirst"` | Optionally change this to ClusterFirstWithHostNet in case you have 'hostNetwork: true'. By default, while using host network, name resolution uses the host's DNS. If you wish nginx-controller to keep resolving names inside the k8s network, use ClusterFirstWithHostNet. |
 | controller.electionID | string | `""` | Election ID to use for status update, by default it uses the controller name combined with a suffix of 'leader' |
diff --git a/charts/ingress-nginx/templates/controller-configmap.yaml b/charts/ingress-nginx/templates/controller-configmap.yaml
index f28b26e1e..ffd003ee8 100644
--- a/charts/ingress-nginx/templates/controller-configmap.yaml
+++ b/charts/ingress-nginx/templates/controller-configmap.yaml
@@ -14,6 +14,7 @@ metadata:
   namespace: {{ .Release.Namespace }}
 data:
   allow-snippet-annotations: "{{ .Values.controller.allowSnippetAnnotations }}"
+  disable-pathtype-validation: "{{ .Values.controller.disablePathTypeValidation }}"
 {{- if .Values.controller.addHeaders }}
   add-headers: {{ .Release.Namespace }}/{{ include "ingress-nginx.fullname" . }}-custom-add-headers
 {{- end }}
diff --git a/charts/ingress-nginx/values.yaml b/charts/ingress-nginx/values.yaml
index 215ce509d..072493ccc 100644
--- a/charts/ingress-nginx/values.yaml
+++ b/charts/ingress-nginx/values.yaml
@@ -87,6 +87,11 @@ controller:
   # Global snippets in ConfigMap are still respected
   allowSnippetAnnotations: true
 
+  # -- This configuration defines if Ingress Controller should validate pathType.
+  # If this is true, special characters will be allowed on paths of any pathType. If
+  # false, special characters are only allowed on paths with pathType = ImplementationSpecific
+  disablePathTypeValidation: false
+
   # -- Required for use with CNI based kubernetes installations (such as ones set up by kubeadm),
   # since CNI and hostport don't mix yet. Can be deprecated once https://github.com/kubernetes/kubernetes/issues/23920
   # is merged
diff --git a/docs/user-guide/nginx-configuration/configmap.md b/docs/user-guide/nginx-configuration/configmap.md
index 5c0b79510..849d6968a 100755
--- a/docs/user-guide/nginx-configuration/configmap.md
+++ b/docs/user-guide/nginx-configuration/configmap.md
@@ -103,7 +103,9 @@ The following table shows a configuration option's name, type, and the default v
 |[brotli-min-length](#brotli-min-length)|int|20|
 |[brotli-types](#brotli-types)|string|"application/xml+rss application/atom+xml application/javascript application/x-javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/svg+xml image/x-icon text/css text/javascript text/plain text/x-component"|
 |[use-http2](#use-http2)|bool|"true"|
+|[gzip-disable](#gzip-disable)|string|""|
 |[gzip-level](#gzip-level)|int|1|
+|[gzip-min-length](#gzip-min-length)|int|256|
 |[gzip-types](#gzip-types)|string|"application/atom+xml application/javascript application/x-javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/svg+xml image/x-icon text/css text/javascript text/plain text/x-component"|
 |[worker-processes](#worker-processes)|string|`<Number of CPUs>`|
 |[worker-cpu-affinity](#worker-cpu-affinity)|string|""|
@@ -214,6 +216,8 @@ The following table shows a configuration option's name, type, and the default v
 |[service-upstream](#service-upstream)|bool|"false"|
 |[ssl-reject-handshake](#ssl-reject-handshake)|bool|"false"|
 |[debug-connections](#debug-connections)|[]string|"127.0.0.1,1.1.1.1/24"|
+|[disable-pathtype-validation](#disable-pathtype-validation)|bool|"false"|
+|[path-additional-allowed-chars](#path-additional-allowed-chars)|string|"^%$[](){}*+?"|
 
 ## add-headers
 
@@ -715,6 +719,10 @@ _**default:**_ `application/xml+rss application/atom+xml application/javascript
 
 Enables or disables [HTTP/2](https://nginx.org/en/docs/http/ngx_http_v2_module.html) support in secure connections.
 
+## gzip-disable
+
+Disables [gzipping](http://nginx.org/en/docs/http/ngx_http_gzip_module.html#gzip_disable) of responses for requests with "User-Agent" header fields matching any of the specified regular expressions.
+
 ## gzip-level
 
 Sets the gzip Compression Level that will be used. _**default:**_ 1
@@ -1320,3 +1328,24 @@ _**default:**_ ""
 
 _References:_
 [http://nginx.org/en/docs/ngx_core_module.html#debug_connection](http://nginx.org/en/docs/ngx_core_module.html#debug_connection)
+
+## disable-pathtype-validation
+Ingress Controller validates the pathType, and only allows special characters on "path" if pathType is
+ImplementationSpecific. 
+
+The only characters allowed on ingresses with pathType not ImplementationSpecific 
+will be 0-9, a-z, A-Z, "-", ".", "_", "~", "/".
+
+If the validation is disabled, the [#path-additional-allowed-chars](#path-additional-allowed-chars) will
+be allowed on any pathType.
+
+This behavior can be disabled, so special characters are accepted regardless of pathType
+_**default:**_ "false"
+
+## path-additional-allowed-chars
+When validating path on Ingress resources, defines the additional set of special characters that
+will be allowed.
+
+See also [#disable-pathtype-validation](#disable-pathtype-validation).
+
+_**default:**_ "^%$[](){}*+?|"
\ No newline at end of file
diff --git a/internal/ingress/controller/config/config.go b/internal/ingress/controller/config/config.go
index f064dad53..b246ef9cf 100644
--- a/internal/ingress/controller/config/config.go
+++ b/internal/ingress/controller/config/config.go
@@ -438,6 +438,11 @@ type Configuration struct {
 	// Default: true
 	UseHTTP2 bool `json:"use-http2,omitempty"`
 
+	// Disables gzipping of responses for requests with "User-Agent" header fields matching any of
+	// the specified regular expressions.
+	// http://nginx.org/en/docs/http/ngx_http_gzip_module.html#gzip_disable
+	GzipDisable string `json:"gzip-disable,omitempty"`
+
 	// gzip Compression Level that will be used
 	GzipLevel int `json:"gzip-level,omitempty"`
 
@@ -777,6 +782,17 @@ type Configuration struct {
 	// http://nginx.org/en/docs/ngx_core_module.html#debug_connection
 	// Default: ""
 	DebugConnections []string `json:"debug-connections"`
+
+	// DisablePathTypeValidation allows the admin to disable the pathType validation.
+	// If PathTypeValidation is enabled, the Controller will only allow alphanumeric
+	// characters on path (0-9, a-z, A-Z, "-", ".", "_", "~", "/")
+	DisablePathTypeValidation bool `json:"disable-pathtype-validation"`
+
+	// PathAdditionalAllowedChars allows the admin to specify what are the additional
+	// characters allowed in case of pathType=ImplementationSpecific.
+	// Case disable-pathtype-validation=true, this characters will be allowed on any path.
+	// Defaults to: "^%$[](){}*+?"
+	PathAdditionalAllowedChars string `json:"path-additional-allowed-chars"`
 }
 
 // NewDefault returns the default nginx configuration
@@ -794,7 +810,6 @@ func NewDefault() Configuration {
 	defGlobalExternalAuth := GlobalExternalAuth{"", "", "", "", "", append(defResponseHeaders, ""), "", "", "", []string{}, map[string]string{}, false}
 
 	cfg := Configuration{
-
 		AllowSnippetAnnotations:          true,
 		AllowBackendServerHeader:         false,
 		AnnotationValueWordBlocklist:     "",
@@ -813,6 +828,8 @@ func NewDefault() Configuration {
 		ClientHeaderTimeout:              60,
 		ClientBodyBufferSize:             "8k",
 		ClientBodyTimeout:                60,
+		DisablePathTypeValidation:        false,
+		PathAdditionalAllowedChars:       "^%$[](){}*+?|",
 		EnableUnderscoresInHeaders:       false,
 		ErrorLogLevel:                    errorLevel,
 		UseForwardedHeaders:              false,
diff --git a/internal/ingress/controller/controller.go b/internal/ingress/controller/controller.go
index dab507cdf..5b22fd17a 100644
--- a/internal/ingress/controller/controller.go
+++ b/internal/ingress/controller/controller.go
@@ -325,6 +325,10 @@ func (n *NGINXController) CheckIngress(ing *networking.Ingress) error {
 
 	k8s.SetDefaultNGINXPathType(ing)
 
+	if err := utilingress.ValidateIngressPath(ing, cfg.DisablePathTypeValidation, cfg.PathAdditionalAllowedChars); err != nil {
+		return fmt.Errorf("ingress contains invalid characters: %s", err)
+	}
+
 	allIngresses := n.store.ListIngresses()
 
 	filter := func(toCheck *ingress.Ingress) bool {
diff --git a/internal/ingress/controller/store/store.go b/internal/ingress/controller/store/store.go
index 13af28137..1006858e8 100644
--- a/internal/ingress/controller/store/store.go
+++ b/internal/ingress/controller/store/store.go
@@ -846,6 +846,11 @@ func (s *k8sStore) syncIngress(ing *networkingv1.Ingress) {
 	copyIng := &networkingv1.Ingress{}
 	ing.ObjectMeta.DeepCopyInto(&copyIng.ObjectMeta)
 
+	if err := ingressutils.ValidateIngressPath(ing, s.backendConfig.DisablePathTypeValidation, s.backendConfig.PathAdditionalAllowedChars); err != nil {
+		klog.Errorf("ingress %s contains invalid path and will be skipped: %s", key, err)
+		return
+	}
+
 	if s.backendConfig.AnnotationValueWordBlocklist != "" {
 		if err := checkBadAnnotationValue(copyIng.Annotations, s.backendConfig.AnnotationValueWordBlocklist); err != nil {
 			klog.Warningf("skipping ingress %s: %s", key, err)
@@ -865,10 +870,6 @@ func (s *k8sStore) syncIngress(ing *networkingv1.Ingress) {
 			if path.Path == "" {
 				copyIng.Spec.Rules[ri].HTTP.Paths[pi].Path = "/"
 			}
-			if !ingressutils.IsSafePath(copyIng, path.Path) {
-				klog.Warningf("ingress %s contains invalid path %s", key, path.Path)
-				return
-			}
 		}
 	}
 
diff --git a/internal/k8s/main.go b/internal/k8s/main.go
index e0d2a1660..6973b6cb7 100644
--- a/internal/k8s/main.go
+++ b/internal/k8s/main.go
@@ -180,9 +180,6 @@ func SetDefaultNGINXPathType(ing *networkingv1.Ingress) {
 				p.PathType = &defaultPathType
 			}
 
-			if *p.PathType == networkingv1.PathTypeImplementationSpecific {
-				p.PathType = &defaultPathType
-			}
 		}
 	}
 }
diff --git a/pkg/util/ingress/ingress.go b/pkg/util/ingress/ingress.go
index 5fb3ee7b9..e16518251 100644
--- a/pkg/util/ingress/ingress.go
+++ b/pkg/util/ingress/ingress.go
@@ -23,7 +23,6 @@ import (
 
 	networkingv1 "k8s.io/api/networking/v1"
 	"k8s.io/apimachinery/pkg/util/sets"
-	"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
 	"k8s.io/ingress-nginx/internal/k8s"
 	"k8s.io/ingress-nginx/internal/net/ssl"
 	"k8s.io/ingress-nginx/pkg/apis/ingress"
@@ -31,15 +30,15 @@ import (
 )
 
 const (
-	alphaNumericChars = `\-\.\_\~a-zA-Z0-9/`
-	regexEnabledChars = `\^\$\[\]\(\)\{\}\*\+`
+	alphaNumericChars = `A-Za-z0-9\-\.\_\~\/` // This is the default allowed set on paths
 )
 
 var (
-	// pathAlphaNumeric is a regex validation of something like "^/[a-zA-Z]+$" on path
-	pathAlphaNumeric = regexp.MustCompile("^/[" + alphaNumericChars + "]*$").MatchString
-	// pathRegexEnabled is a regex validation of paths that may contain regex.
-	pathRegexEnabled = regexp.MustCompile("^/[" + alphaNumericChars + regexEnabledChars + "]*$").MatchString
+	// pathAlphaNumeric is a regex validation that allows only (0-9, a-z, A-Z, "-", ".", "_", "~", "/")
+	pathAlphaNumericRegex = regexp.MustCompile("^[" + alphaNumericChars + "]*$").MatchString
+
+	// default path type is Prefix to not break existing definitions
+	defaultPathType = networkingv1.PathTypePrefix
 )
 
 func GetRemovedHosts(rucfg, newcfg *ingress.Configuration) []string {
@@ -247,12 +246,45 @@ func BuildRedirects(servers []*ingress.Server) []*redirect {
 	return redirectServers
 }
 
-// IsSafePath verifies if the path used in ingress object contains only valid characters.
-// It will behave differently if regex is enabled or not
-func IsSafePath(copyIng *networkingv1.Ingress, path string) bool {
-	isRegex, _ := parser.GetBoolAnnotation("use-regex", copyIng)
-	if isRegex {
-		return pathRegexEnabled(path)
+func ValidateIngressPath(copyIng *networkingv1.Ingress, disablePathTypeValidation bool, additionalChars string) error {
+
+	if copyIng == nil {
+		return nil
+	}
+
+	escapedAdditionalChars := regexp.QuoteMeta(additionalChars)
+	regexPath, err := regexp.Compile("^[" + alphaNumericChars + escapedAdditionalChars + "]*$")
+	if err != nil {
+		return fmt.Errorf("ingress has misconfigured validation regex on configmap: %s - %w", additionalChars, err)
+	}
+
+	for _, rule := range copyIng.Spec.Rules {
+		if rule.HTTP == nil {
+			continue
+		}
+		if err := checkPath(rule.HTTP.Paths, disablePathTypeValidation, regexPath); err != nil {
+			return fmt.Errorf("error validating ingressPath: %w", err)
+		}
+	}
+	return nil
+}
+
+func checkPath(paths []networkingv1.HTTPIngressPath, disablePathTypeValidation bool, regexSpecificChars *regexp.Regexp) error {
+	for _, path := range paths {
+		if path.PathType == nil {
+			path.PathType = &defaultPathType
+		}
+
+		if disablePathTypeValidation || *path.PathType == networkingv1.PathTypeImplementationSpecific {
+			if !regexSpecificChars.MatchString(path.Path) {
+				return fmt.Errorf("path %s of type %s contains invalid characters", path.Path, *path.PathType)
+			}
+			continue
+		}
+
+		if !pathAlphaNumericRegex(path.Path) {
+			return fmt.Errorf("path %s of type %s contains invalid characters", path.Path, *path.PathType)
+		}
 	}
-	return pathAlphaNumeric(path)
+	return nil
 }
diff --git a/rootfs/etc/nginx/template/nginx.tmpl b/rootfs/etc/nginx/template/nginx.tmpl
index fc48de912..911cf75ea 100755
--- a/rootfs/etc/nginx/template/nginx.tmpl
+++ b/rootfs/etc/nginx/template/nginx.tmpl
@@ -336,6 +336,9 @@ http {
     {{ if $cfg.UseGzip }}
     gzip on;
     gzip_comp_level {{ $cfg.GzipLevel }};
+    {{- if $cfg.GzipDisable }}
+    gzip_disable "{{ $cfg.GzipDisable }}";
+    {{- end }}
     gzip_http_version 1.1;
     gzip_min_length {{ $cfg.GzipMinLength}};
     gzip_types {{ $cfg.GzipTypes }};


Test patch:
diff --git a/internal/ingress/controller/controller_test.go b/internal/ingress/controller/controller_test.go
index 8cca10385..d91760552 100644
--- a/internal/ingress/controller/controller_test.go
+++ b/internal/ingress/controller/controller_test.go
@@ -201,6 +201,97 @@ func TestCheckIngress(t *testing.T) {
 			},
 		},
 	}
+
+	t.Run("when validating pathType", func(t *testing.T) {
+		t.Run("When ingress contains invalid path and pathType validation is not disabled", func(t *testing.T) {
+			nginx.store = fakeIngressStore{
+				ingresses: []*ingress.Ingress{},
+				configuration: ngx_config.Configuration{
+					DisablePathTypeValidation: false,
+				},
+			}
+			nginx.command = testNginxTestCommand{
+				t:        t,
+				err:      nil,
+				expected: "",
+			}
+			nginx.cfg.IngressClassConfiguration = &ingressclass.IngressClassConfiguration{
+				WatchWithoutClass: true,
+			}
+			ingPath := &networking.Ingress{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-ingress1",
+					Namespace: "user-namespace1",
+					Annotations: map[string]string{
+						"kubernetes.io/ingress.class": "nginx",
+					},
+				},
+				Spec: networking.IngressSpec{
+					Rules: []networking.IngressRule{
+						{
+							Host: "example.com",
+							IngressRuleValue: networking.IngressRuleValue{
+								HTTP: &networking.HTTPIngressRuleValue{
+									Paths: []networking.HTTPIngressPath{
+										{
+											Path:     "/xpto/(a+)",
+											PathType: &pathTypePrefix,
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			}
+
+			if nginx.CheckIngress(ingPath) == nil {
+				t.Errorf("invalid path on pathTypePrefix and validation enabled should return an error")
+			}
+		})
+		t.Run("When ingress contains invalid path and pathType validation is disabled", func(t *testing.T) {
+			nginx.store = fakeIngressStore{
+				ingresses: []*ingress.Ingress{},
+				configuration: ngx_config.Configuration{
+					DisablePathTypeValidation:  true,
+					PathAdditionalAllowedChars: "^%$[](){}*+?|",
+				},
+			}
+			nginx.command = testNginxTestCommand{
+				t:        t,
+				err:      nil,
+				expected: "_,example.com",
+			}
+
+			ingPath := &networking.Ingress{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-ingress2",
+					Namespace: "user-namespace2",
+				},
+				Spec: networking.IngressSpec{
+					Rules: []networking.IngressRule{
+						{
+							Host: "example.com",
+							IngressRuleValue: networking.IngressRuleValue{
+								HTTP: &networking.HTTPIngressRuleValue{
+									Paths: []networking.HTTPIngressPath{
+										{
+											Path:     "/example(/|$)(.*)",
+											PathType: &pathTypePrefix,
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			}
+
+			if nginx.CheckIngress(ingPath) != nil {
+				t.Errorf("invalid path on pathTypePrefix and validation disabled should not return an error: %s", nginx.CheckIngress(ingPath))
+			}
+		})
+	})
 	t.Run("when the class is the nginx one", func(t *testing.T) {
 		ing.ObjectMeta.Annotations["kubernetes.io/ingress.class"] = "nginx"
 		nginx.command = testNginxTestCommand{
diff --git a/internal/ingress/controller/template/configmap_test.go b/internal/ingress/controller/template/configmap_test.go
index c662e0bc7..ebe55d192 100644
--- a/internal/ingress/controller/template/configmap_test.go
+++ b/internal/ingress/controller/template/configmap_test.go
@@ -64,6 +64,7 @@ func TestMergeConfigMapToStruct(t *testing.T) {
 		"access-log-path":               "/var/log/test/access.log",
 		"error-log-path":                "/var/log/test/error.log",
 		"use-gzip":                      "false",
+		"gzip-disable":                  "msie6",
 		"gzip-level":                    "9",
 		"gzip-min-length":               "1024",
 		"gzip-types":                    "text/html",
@@ -87,6 +88,7 @@ func TestMergeConfigMapToStruct(t *testing.T) {
 	def.ProxyReadTimeout = 1
 	def.ProxySendTimeout = 2
 	def.UseProxyProtocol = true
+	def.GzipDisable = "msie6"
 	def.GzipLevel = 9
 	def.GzipMinLength = 1024
 	def.GzipTypes = "text/html"
diff --git a/pkg/util/ingress/ingress_test.go b/pkg/util/ingress/ingress_test.go
index d829a57f1..a79da6b6f 100644
--- a/pkg/util/ingress/ingress_test.go
+++ b/pkg/util/ingress/ingress_test.go
@@ -17,13 +17,10 @@ limitations under the License.
 package ingress
 
 import (
-	"fmt"
 	"testing"
 
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-
 	networkingv1 "k8s.io/api/networking/v1"
-	"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/ingress-nginx/pkg/apis/ingress"
 )
 
@@ -136,81 +133,168 @@ func TestIsDynamicConfigurationEnough(t *testing.T) {
 	}
 }
 
-func generateDumbIngressforPathTest(regexEnabled bool) *networkingv1.Ingress {
-	var annotations = make(map[string]string)
-	regexAnnotation := fmt.Sprintf("%s/use-regex", parser.AnnotationsPrefix)
-	if regexEnabled {
-		annotations[regexAnnotation] = "true"
-	}
+func generateDumbIngressforPathTest(pathType *networkingv1.PathType, path string) *networkingv1.Ingress {
 	return &networkingv1.Ingress{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:        "dumb",
-			Namespace:   "default",
-			Annotations: annotations,
+			Name:      "dumb",
+			Namespace: "default",
+		},
+		Spec: networkingv1.IngressSpec{
+			Rules: []networkingv1.IngressRule{
+				{
+					Host: "test.com",
+					IngressRuleValue: networkingv1.IngressRuleValue{
+						HTTP: &networkingv1.HTTPIngressRuleValue{
+							Paths: []networkingv1.HTTPIngressPath{
+								{
+									PathType: pathType,
+									Path:     path,
+								},
+							},
+						},
+					},
+				},
+			},
 		},
 	}
 }
 
-func TestIsSafePath(t *testing.T) {
+func generateComplexIngress(ing *networkingv1.Ingress) *networkingv1.Ingress {
+
+	oldRules := ing.Spec.DeepCopy().Rules
+	ing.Spec.Rules = []networkingv1.IngressRule{
+		{
+			Host: "test1.com",
+			IngressRuleValue: networkingv1.IngressRuleValue{
+				HTTP: &networkingv1.HTTPIngressRuleValue{
+					Paths: []networkingv1.HTTPIngressPath{
+						{
+							PathType: &pathTypeExact,
+							Path:     "/xpto",
+						},
+					},
+				},
+			},
+		},
+		{
+			Host: "test2.com",
+			IngressRuleValue: networkingv1.IngressRuleValue{
+				HTTP: &networkingv1.HTTPIngressRuleValue{
+					Paths: []networkingv1.HTTPIngressPath{
+						{
+							PathType: &pathTypeExact,
+							Path:     "/someotherpath",
+						},
+						{
+							PathType: &pathTypePrefix,
+							Path:     "/someprefix/~xpto/lala123",
+						},
+					},
+				},
+			},
+		},
+	}
+	// we want to invert the order to test better :)
+	ing.Spec.Rules = append(ing.Spec.Rules, oldRules...)
+
+	return ing
+}
+
+var (
+	pathTypeExact        = networkingv1.PathTypeExact
+	pathTypePrefix       = networkingv1.PathTypePrefix
+	pathTypeImplSpecific = networkingv1.PathTypeImplementationSpecific
+)
+
+const (
+	defaultAdditionalChars = "^%$[](){}*+?"
+)
+
+func TestValidateIngressPath(t *testing.T) {
 	tests := []struct {
-		name    string
-		copyIng *networkingv1.Ingress
-		path    string
-		want    bool
+		name                      string
+		copyIng                   *networkingv1.Ingress
+		disablePathTypeValidation bool
+		additionalChars           string
+		wantErr                   bool
 	}{
 		{
-			name:    "should accept valid path with regex disabled",
-			want:    true,
-			copyIng: generateDumbIngressforPathTest(false),
-			path:    "/xpto/~user/t-e_st.exe",
+			name:    "should return nil when ingress = nil",
+			wantErr: false,
+			copyIng: nil,
+		},
+		{
+			name:    "should accept valid path on pathType Exact",
+			wantErr: false,
+			copyIng: generateDumbIngressforPathTest(&pathTypeExact, "/xpto/~user9/t-e_st.exe"),
+		},
+		{
+			name:    "should accept valid path on pathType Prefix",
+			wantErr: false,
+			copyIng: generateDumbIngressforPathTest(&pathTypePrefix, "/xpto/~user9/t-e_st.exe"),
+		},
+		{
+			name:    "should accept valid simple path on pathType Impl Specific",
+			wantErr: false,
+			copyIng: generateDumbIngressforPathTest(&pathTypeImplSpecific, "/xpto/~user9/t-e_st.exe"),
+		},
+		{
+			name:    "should accept valid path on pathType nil",
+			wantErr: false,
+			copyIng: generateDumbIngressforPathTest(nil, "/xpto/~user/t-e_st.exe"),
+		},
+		{
+			name:    "should accept empty path",
+			wantErr: false,
+			copyIng: generateDumbIngressforPathTest(&pathTypePrefix, ""),
 		},
 		{
-			name:    "should accept valid path / with regex disabled",
-			want:    true,
-			copyIng: generateDumbIngressforPathTest(false),
-			path:    "/",
+			name:            "should deny path with bad characters and pathType not implementationSpecific",
+			wantErr:         true,
+			additionalChars: defaultAdditionalChars,
+			copyIng:         generateDumbIngressforPathTest(&pathTypeExact, "/foo/bar/(.+)"),
 		},
 		{
-			name:    "should reject invalid path with invalid chars",
-			want:    false,
-			copyIng: generateDumbIngressforPathTest(false),
-			path:    "/foo/bar/;xpto",
+			name:            "should accept path with regex characters and pathType implementationSpecific",
+			wantErr:         false,
+			additionalChars: defaultAdditionalChars,
+			copyIng:         generateDumbIngressforPathTest(&pathTypeImplSpecific, "/foo/bar/(.+)"),
 		},
 		{
-			name:    "should reject regex path when regex is disabled",
-			want:    false,
-			copyIng: generateDumbIngressforPathTest(false),
-			path:    "/foo/bar/(.+)",
+			name:                      "should accept path with regex characters and pathType exact, but pathType validation disabled",
+			wantErr:                   false,
+			additionalChars:           defaultAdditionalChars,
+			disablePathTypeValidation: true,
+			copyIng:                   generateDumbIngressforPathTest(&pathTypeImplSpecific, "/foo/bar/(.+)"),
 		},
 		{
-			name:    "should accept valid path / with regex enabled",
-			want:    true,
-			copyIng: generateDumbIngressforPathTest(true),
-			path:    "/",
+			name:                      "should reject path when the allowed additional set does not match",
+			wantErr:                   true,
+			additionalChars:           "().?",
+			disablePathTypeValidation: false,
+			copyIng:                   generateDumbIngressforPathTest(&pathTypeImplSpecific, "/foo/bar/(.+)"),
 		},
 		{
-			name:    "should accept regex path when regex is enabled",
-			want:    true,
-			copyIng: generateDumbIngressforPathTest(true),
-			path:    "/foo/bar/(.+)",
+			name:            "should accept path when the allowed additional set does match",
+			wantErr:         false,
+			additionalChars: "().?",
+			copyIng:         generateDumbIngressforPathTest(&pathTypeImplSpecific, "/foo/bar/(.?)"),
 		},
 		{
-			name:    "should reject regex path when regex is enabled but the path is invalid",
-			want:    false,
-			copyIng: generateDumbIngressforPathTest(true),
-			path:    "/foo/bar/;xpto",
+			name:    "should block if at least one path is bad",
+			wantErr: true,
+			copyIng: generateComplexIngress(generateDumbIngressforPathTest(&pathTypeExact, "/foo/bar/(.?)")),
 		},
 		{
-			name:    "should reject regex path when regex is enabled but the path is invalid",
-			want:    false,
-			copyIng: generateDumbIngressforPathTest(true),
-			path:    ";xpto",
+			name:    "should block if at least one path is bad",
+			wantErr: true,
+			copyIng: generateComplexIngress(generateDumbIngressforPathTest(&pathTypeImplSpecific, "/foo/bar/(.?)")),
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			if got := IsSafePath(tt.copyIng, tt.path); got != tt.want {
-				t.Errorf("IsSafePath() = %v, want %v", got, tt.want)
+			if err := ValidateIngressPath(tt.copyIng, tt.disablePathTypeValidation, tt.additionalChars); (err != nil) != tt.wantErr {
+				t.Errorf("ValidateIngressPath() error = %v, wantErr %v", err, tt.wantErr)
 			}
 		})
 	}
diff --git a/test/e2e/admission/admission.go b/test/e2e/admission/admission.go
index bde98fddf..5c037a0c5 100644
--- a/test/e2e/admission/admission.go
+++ b/test/e2e/admission/admission.go
@@ -30,6 +30,14 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"k8s.io/ingress-nginx/test/e2e/framework"
+
+	networkingv1 "k8s.io/api/networking/v1"
+)
+
+var (
+	pathExact        = networkingv1.PathTypeExact
+	pathPrefix       = networkingv1.PathTypePrefix
+	pathImplSpecific = networkingv1.PathTypeImplementationSpecific
 )
 
 var _ = framework.IngressNginxDescribe("[Serial] admission controller", func() {
@@ -152,6 +160,32 @@ var _ = framework.IngressNginxDescribe("[Serial] admission controller", func() {
 		assert.NotNil(ginkgo.GinkgoT(), err, "creating an ingress with invalid annotation value should return an error")
 	})
 
+	ginkgo.It("should reject ingress with bad characters and pathType != ImplementationSpecific", func() {
+		host := "admission-test"
+
+		firstIngress := framework.NewSingleIngress("first-ingress", "/xpto*", host, f.Namespace, framework.EchoService, 80, nil)
+		firstIngress.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].PathType = &pathPrefix
+		_, err := f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Create(context.TODO(), firstIngress, metav1.CreateOptions{})
+		assert.NotNil(ginkgo.GinkgoT(), err, "creating an ingress with invalid path value should return an error")
+
+		secondIngress := framework.NewSingleIngress("second-ingress", "/abc123*", host, f.Namespace, framework.EchoService, 80, nil)
+		secondIngress.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].PathType = &pathImplSpecific
+		_, err = f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Create(context.TODO(), secondIngress, metav1.CreateOptions{})
+		assert.Nil(ginkgo.GinkgoT(), err, "creating an ingress with regex on path and pathType ImplementationSpecific should not return an error")
+
+	})
+
+	ginkgo.It("should not validate characters on ingress when validation of pathType is disabled", func() {
+		host := "admission-test"
+
+		f.UpdateNginxConfigMapData("disable-pathtype-validation", "true")
+
+		firstIngress := framework.NewSingleIngress("first-ingress", "/xpto*", host, f.Namespace, framework.EchoService, 80, nil)
+		firstIngress.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].PathType = &pathPrefix
+		_, err := f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Create(context.TODO(), firstIngress, metav1.CreateOptions{})
+		assert.Nil(ginkgo.GinkgoT(), err, "creating an ingress with regex chars on path and pathType validation disabled should be accepted")
+	})
+
 	ginkgo.It("should return an error if there is a forbidden value in some annotation", func() {
 		host := "admission-test"
 
diff --git a/test/e2e/annotations/affinity.go b/test/e2e/annotations/affinity.go
index 3e1e9e969..31e8905d1 100644
--- a/test/e2e/annotations/affinity.go
+++ b/test/e2e/annotations/affinity.go
@@ -35,6 +35,8 @@ import (
 var _ = framework.DescribeAnnotation("affinity session-cookie-name", func() {
 	f := framework.NewDefaultFramework("affinity")
 
+	pathImpl := networking.PathTypeImplementationSpecific
+
 	ginkgo.BeforeEach(func() {
 		f.NewEchoDeployment(framework.WithDeploymentReplicas(2))
 	})
@@ -276,6 +278,8 @@ var _ = framework.DescribeAnnotation("affinity session-cookie-name", func() {
 		annotations["nginx.ingress.kubernetes.io/session-cookie-path"] = "/foo/bar"
 
 		ing := framework.NewSingleIngress(host, "/foo/.*", host, f.Namespace, framework.EchoService, 80, annotations)
+		ing.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].PathType = &pathImpl
+
 		f.EnsureIngress(ing)
 
 		f.WaitForNginxServer(host,
@@ -299,6 +303,8 @@ var _ = framework.DescribeAnnotation("affinity session-cookie-name", func() {
 		annotations["nginx.ingress.kubernetes.io/use-regex"] = "true"
 
 		ing := framework.NewSingleIngress(host, "/foo/.*", host, f.Namespace, framework.EchoService, 80, annotations)
+		ing.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].PathType = &pathImpl
+
 		f.EnsureIngress(ing)
 
 		f.WaitForNginxServer(host,
diff --git a/test/e2e/annotations/rewrite.go b/test/e2e/annotations/rewrite.go
index 79738b984..0c426a8df 100644
--- a/test/e2e/annotations/rewrite.go
+++ b/test/e2e/annotations/rewrite.go
@@ -24,12 +24,15 @@ import (
 	"github.com/onsi/ginkgo/v2"
 	"github.com/stretchr/testify/assert"
 
+	networking "k8s.io/api/networking/v1"
 	"k8s.io/ingress-nginx/test/e2e/framework"
 )
 
 var _ = framework.DescribeAnnotation("rewrite-target use-regex enable-rewrite-log", func() {
 	f := framework.NewDefaultFramework("rewrite")
 
+	pathImpl := networking.PathTypeImplementationSpecific
+
 	ginkgo.BeforeEach(func() {
 		f.NewEchoDeployment()
 	})
@@ -126,6 +129,7 @@ var _ = framework.DescribeAnnotation("rewrite-target use-regex enable-rewrite-lo
 			"nginx.ingress.kubernetes.io/rewrite-target": "/new/backend",
 		}
 		ing = framework.NewSingleIngress("regex", "/foo.+", host, f.Namespace, framework.EchoService, 80, annotations)
+		ing.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].PathType = &pathImpl
 		f.EnsureIngress(ing)
 
 		f.WaitForNginxServer(host,
@@ -168,6 +172,8 @@ var _ = framework.DescribeAnnotation("rewrite-target use-regex enable-rewrite-lo
 			"nginx.ingress.kubernetes.io/rewrite-target": "/new/backend",
 		}
 		ing = framework.NewSingleIngress("regex", "/foo/bar/[a-z]{3}", host, f.Namespace, framework.EchoService, 80, annotations)
+		ing.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].PathType = &pathImpl
+
 		f.EnsureIngress(ing)
 
 		f.WaitForNginxServer(host,
@@ -196,6 +202,8 @@ var _ = framework.DescribeAnnotation("rewrite-target use-regex enable-rewrite-lo
 			"nginx.ingress.kubernetes.io/rewrite-target": "/new/backend/$1",
 		}
 		ing := framework.NewSingleIngress("regex", "/foo/bar/(.+)", host, f.Namespace, framework.EchoService, 80, annotations)
+		ing.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].PathType = &pathImpl
+
 		f.EnsureIngress(ing)
 
 		f.WaitForNginxServer(host,
diff --git a/test/e2e/security/invalid_paths.go b/test/e2e/security/invalid_paths.go
deleted file mode 100644
index d75aefc2c..000000000
--- a/test/e2e/security/invalid_paths.go
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
-Copyright 2022 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package security
-
-import (
-	"fmt"
-	"net/http"
-	"strings"
-
-	"github.com/onsi/ginkgo/v2"
-
-	"k8s.io/ingress-nginx/test/e2e/framework"
-)
-
-const (
-	validPath   = "/xpto/~user/t-e_st.exe"
-	invalidPath = "/foo/bar/;xpto"
-	regexPath   = "/foo/bar/(.+)"
-	host        = "securitytest.com"
-)
-
-var (
-	annotationRegex = map[string]string{
-		"nginx.ingress.kubernetes.io/use-regex": "true",
-	}
-)
-
-var _ = framework.IngressNginxDescribe("[Security] validate path fields", func() {
-	f := framework.NewDefaultFramework("validate-path")
-
-	ginkgo.BeforeEach(func() {
-		f.NewEchoDeployment()
-	})
-
-	ginkgo.It("should accept an ingress with valid path", func() {
-
-		ing := framework.NewSingleIngress(host, validPath, host, f.Namespace, framework.EchoService, 80, nil)
-
-		f.EnsureIngress(ing)
-
-		f.WaitForNginxServer(host,
-			func(server string) bool {
-				return strings.Contains(server, fmt.Sprintf("server_name %s ;", host))
-			})
-
-		f.HTTPTestClient().
-			GET(validPath).
-			WithHeader("Host", host).
-			Expect().
-			Status(http.StatusOK)
-	})
-
-	ginkgo.It("should drop an ingress with invalid path", func() {
-
-		ing := framework.NewSingleIngress(host, invalidPath, host, f.Namespace, framework.EchoService, 80, nil)
-		f.EnsureIngress(ing)
-
-		f.WaitForNginxServer(host,
-			func(server string) bool {
-				return !strings.Contains(server, fmt.Sprintf("server_name %s ;", host))
-			})
-
-		f.HTTPTestClient().
-			GET(invalidPath).
-			WithHeader("Host", host).
-			Expect().
-			Status(http.StatusNotFound)
-	})
-
-	ginkgo.It("should drop an ingress with regex path and regex disabled", func() {
-
-		ing := framework.NewSingleIngress(host, regexPath, host, f.Namespace, framework.EchoService, 80, nil)
-		f.EnsureIngress(ing)
-
-		f.WaitForNginxServer(host,
-			func(server string) bool {
-				return !strings.Contains(server, fmt.Sprintf("server_name %s ;", host))
-			})
-
-		f.HTTPTestClient().
-			GET("/foo/bar/lalala").
-			WithHeader("Host", host).
-			Expect().
-			Status(http.StatusNotFound)
-	})
-
-	ginkgo.It("should accept an ingress with regex path and regex enabled", func() {
-
-		ing := framework.NewSingleIngress(host, regexPath, host, f.Namespace, framework.EchoService, 80, annotationRegex)
-		f.EnsureIngress(ing)
-
-		f.WaitForNginxServer(host,
-			func(server string) bool {
-				return strings.Contains(server, fmt.Sprintf("server_name %s ;", host))
-			})
-
-		f.HTTPTestClient().
-			GET("/foo/bar/lalala").
-			WithHeader("Host", host).
-			Expect().
-			Status(http.StatusOK)
-	})
-
-	ginkgo.It("should reject an ingress with invalid path and regex enabled", func() {
-
-		ing := framework.NewSingleIngress(host, invalidPath, host, f.Namespace, framework.EchoService, 80, annotationRegex)
-		f.EnsureIngress(ing)
-
-		f.WaitForNginxServer(host,
-			func(server string) bool {
-				return !strings.Contains(server, fmt.Sprintf("server_name %s ;", host))
-			})
-
-		f.HTTPTestClient().
-			GET(invalidPath).
-			WithHeader("Host", host).
-			Expect().
-			Status(http.StatusNotFound)
-	})
-})
diff --git a/test/e2e/settings/gzip.go b/test/e2e/settings/gzip.go
new file mode 100644
index 000000000..68e80d3a0
--- /dev/null
+++ b/test/e2e/settings/gzip.go
@@ -0,0 +1,99 @@
+/*
+Copyright 2023 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package settings
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/onsi/ginkgo/v2"
+
+	"k8s.io/ingress-nginx/internal/ingress/controller/config"
+	"k8s.io/ingress-nginx/test/e2e/framework"
+)
+
+var _ = framework.DescribeSetting("gzip", func() {
+	f := framework.NewDefaultFramework("gzip")
+
+	ginkgo.It("should be disabled by default", func() {
+		f.WaitForNginxConfiguration(
+			func(cfg string) bool {
+				return !strings.Contains(cfg, "gzip on;")
+			})
+	})
+
+	ginkgo.It("should be enabled with default settings", func() {
+		f.UpdateNginxConfigMapData("use-gzip", "true")
+
+		f.WaitForNginxConfiguration(
+			func(cfg string) bool {
+				defaultCfg := config.NewDefault()
+				return strings.Contains(cfg, "gzip on;") &&
+					strings.Contains(cfg, fmt.Sprintf("gzip_comp_level %d;", defaultCfg.GzipLevel)) &&
+					!strings.Contains(cfg, "gzip_disable") &&
+					strings.Contains(cfg, "gzip_http_version 1.1;") &&
+					strings.Contains(cfg, fmt.Sprintf("gzip_min_length %d;", defaultCfg.GzipMinLength)) &&
+					strings.Contains(cfg, fmt.Sprintf("gzip_types %s;", defaultCfg.GzipTypes)) &&
+					strings.Contains(cfg, "gzip_proxied any;") &&
+					strings.Contains(cfg, "gzip_vary on;")
+			})
+	})
+
+	ginkgo.It("should set gzip_comp_level to 4", func() {
+		f.UpdateNginxConfigMapData("use-gzip", "true")
+		f.UpdateNginxConfigMapData("gzip-level", "4")
+
+		f.WaitForNginxConfiguration(
+			func(cfg string) bool {
+				return strings.Contains(cfg, "gzip on;") &&
+					strings.Contains(cfg, "gzip_comp_level 4;")
+			})
+	})
+
+	ginkgo.It("should set gzip_disable to msie6", func() {
+		f.UpdateNginxConfigMapData("use-gzip", "true")
+		f.UpdateNginxConfigMapData("gzip-disable", "msie6")
+
+		f.WaitForNginxConfiguration(
+			func(cfg string) bool {
+				return strings.Contains(cfg, "gzip on;") &&
+					strings.Contains(cfg, `gzip_disable "msie6";`)
+			})
+	})
+
+	ginkgo.It("should set gzip_min_length to 100", func() {
+		f.UpdateNginxConfigMapData("use-gzip", "true")
+		f.UpdateNginxConfigMapData("gzip-min-length", "100")
+
+		f.WaitForNginxConfiguration(
+			func(cfg string) bool {
+				return strings.Contains(cfg, "gzip on;") &&
+					strings.Contains(cfg, "gzip_min_length 100;")
+			})
+	})
+
+	ginkgo.It("should set gzip_types to application/javascript", func() {
+		f.UpdateNginxConfigMapData("use-gzip", "true")
+		f.UpdateNginxConfigMapData("gzip-types", "application/javascript")
+
+		f.WaitForNginxConfiguration(
+			func(cfg string) bool {
+				return strings.Contains(cfg, "gzip on;") &&
+					strings.Contains(cfg, "gzip_types application/javascript;")
+			})
+	})
+})