-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add option to sanitize annotation inputs #7874
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -237,12 +237,25 @@ func (n *NGINXController) CheckIngress(ing *networking.Ingress) error { | |
cfg := n.store.GetBackendConfiguration() | ||
cfg.Resolver = n.resolver | ||
|
||
for key := range ing.ObjectMeta.GetAnnotations() { | ||
for key, value := range ing.ObjectMeta.GetAnnotations() { | ||
|
||
if parser.AnnotationsPrefix != parser.DefaultAnnotationsPrefix { | ||
if strings.HasPrefix(key, fmt.Sprintf("%s/", parser.DefaultAnnotationsPrefix)) { | ||
return fmt.Errorf("This deployment has a custom annotation prefix defined. Use '%s' instead of '%s'", parser.AnnotationsPrefix, parser.DefaultAnnotationsPrefix) | ||
} | ||
} | ||
if strings.HasPrefix(key, fmt.Sprintf("%s/", parser.AnnotationsPrefix)) { | ||
for _, forbiddenvalue := range cfg.AnnotationValueWordBlocklist { | ||
if strings.Contains(value, forbiddenvalue) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My only thought is if we need to run There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you give me one example? not following here (rainy day, a bit sleepy... :) ) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here’s what I mean: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @theunrealgeek GOOD CATCH!!! Thanks! I will quickly fix this and also add some regression / e2e test before releasing! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But now I see that I do trimming a bit upper:
Don't this solve the problem? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
will only trim space at the beginning and end of the annotation value, but not in between elements that may exist around the commas which is what my example in the Go playground is pointing out. In the default value this won’t be a problem since you are coming the CSV with a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok, I see :) I was in my head thinking that trimSpace works for all the array, not only beginning and ending and you are right :) Weirdly, when adding this to unit_test it passes as well, will have to check it better. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Anyway, PTAL here: #7921 |
||
return fmt.Errorf("%s annotation contains invalid word %s", key, forbiddenvalue) | ||
} | ||
} | ||
for _, invalidchar := range cfg.AnnotationValueCharBlocklist { | ||
if strings.ContainsAny(value, invalidchar) { | ||
return fmt.Errorf("%s annotation contains invalid character %s", key, invalidchar) | ||
} | ||
} | ||
} | ||
|
||
if !cfg.AllowSnippetAnnotations && strings.HasSuffix(key, "-snippet") { | ||
return fmt.Errorf("%s annotation cannot be used. Snippet directives are disabled by the Ingress administrator", key) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -268,6 +268,40 @@ func TestCheckIngress(t *testing.T) { | |
} | ||
}) | ||
|
||
t.Run("When invalid directives are used in annotation values", func(t *testing.T) { | ||
nginx.store = fakeIngressStore{ | ||
ingresses: []*ingress.Ingress{}, | ||
configuration: ngx_config.Configuration{ | ||
AnnotationValueWordBlocklist: []string{"invalid_directive"}, | ||
}, | ||
} | ||
nginx.command = testNginxTestCommand{ | ||
t: t, | ||
err: nil, | ||
} | ||
ing.ObjectMeta.Annotations["nginx.ingress.kubernetes.io/custom-headers"] = "invalid_directive" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With context of my previous comment, checking for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://github.com/kubernetes/ingress-nginx/pull/7921/files Added here just to make sure it's working fine :) |
||
if err := nginx.CheckIngress(ing); err == nil { | ||
t.Errorf("with an invalid value in annotation the ingress should be rejected") | ||
} | ||
}) | ||
|
||
t.Run("When invalid chars are used in annotation values", func(t *testing.T) { | ||
nginx.store = fakeIngressStore{ | ||
ingresses: []*ingress.Ingress{}, | ||
configuration: ngx_config.Configuration{ | ||
AnnotationValueCharBlocklist: []string{"{", "}"}, | ||
}, | ||
} | ||
nginx.command = testNginxTestCommand{ | ||
t: t, | ||
err: nil, | ||
} | ||
ing.ObjectMeta.Annotations["nginx.ingress.kubernetes.io/custom-headers"] = "{differentheader" | ||
if err := nginx.CheckIngress(ing); err == nil { | ||
t.Errorf("with an invalid chars in annotation the ingress should be rejected") | ||
} | ||
}) | ||
|
||
t.Run("When a new catch-all ingress is being created despite catch-alls being disabled ", func(t *testing.T) { | ||
backendBefore := ing.Spec.DefaultBackend | ||
disableCatchAllBefore := nginx.cfg.DisableCatchAll | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -121,6 +121,28 @@ var _ = framework.IngressNginxDescribe("[Serial] admission controller", func() { | |
assert.NotNil(ginkgo.GinkgoT(), err, "creating an ingress with invalid configuration should return an error") | ||
}) | ||
|
||
ginkgo.It("should return an error if there is an invalid value in some annotation", func() { | ||
host := "admission-test" | ||
|
||
annotations := map[string]string{ | ||
"nginx.ingress.kubernetes.io/connection-proxy-header": "a;}", | ||
} | ||
firstIngress := framework.NewSingleIngress("first-ingress", "/", host, f.Namespace, framework.EchoService, 80, annotations) | ||
_, err := f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Create(context.TODO(), firstIngress, metav1.CreateOptions{}) | ||
assert.NotNil(ginkgo.GinkgoT(), err, "creating an ingress with invalid annotation value should return an error") | ||
}) | ||
|
||
ginkgo.It("should return an error if there is a forbidden value in some annotation", func() { | ||
host := "admission-test" | ||
|
||
annotations := map[string]string{ | ||
"nginx.ingress.kubernetes.io/connection-proxy-header": "set_by_lua", | ||
} | ||
firstIngress := framework.NewSingleIngress("first-ingress", "/", host, f.Namespace, framework.EchoService, 80, annotations) | ||
_, err := f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Create(context.TODO(), firstIngress, metav1.CreateOptions{}) | ||
assert.NotNil(ginkgo.GinkgoT(), err, "creating an ingress with invalid annotation value should return an error") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it make sense to check if the correct err (message) is thrown? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hum, I think we can do that yes. I would leave this anyway as a followup for some issue (feel free to open it!) to improve e2e tests in a sense that we not only verify if the error is not nil, but if error is what we expect (in all tests). This can become a good first issue, wdyt? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good Idea, I will open one :) |
||
}) | ||
|
||
ginkgo.It("should not return an error if the Ingress V1 definition is valid with Ingress Class", func() { | ||
err := createIngress(f.Namespace, validV1Ingress) | ||
assert.Nil(ginkgo.GinkgoT(), err, "creating an ingress using kubectl") | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
/* | ||
Copyright 2021 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" | ||
"net/http" | ||
"strings" | ||
|
||
"github.com/onsi/ginkgo" | ||
|
||
"k8s.io/ingress-nginx/test/e2e/framework" | ||
) | ||
|
||
var _ = framework.DescribeAnnotation("Bad annotation values", func() { | ||
f := framework.NewDefaultFramework("bad-annotation") | ||
|
||
ginkgo.BeforeEach(func() { | ||
f.NewEchoDeployment() | ||
}) | ||
|
||
ginkgo.It("should drop an ingress if there is an invalid character in some annotation", func() { | ||
host := "invalid-value-test" | ||
|
||
annotations := map[string]string{ | ||
"nginx.ingress.kubernetes.io/configuration-snippet": ` | ||
# abc { }`, | ||
} | ||
|
||
ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) | ||
f.UpdateNginxConfigMapData("allow-snippet-annotations", "true") | ||
f.EnsureIngress(ing) | ||
|
||
f.WaitForNginxServer(host, | ||
func(server string) bool { | ||
return !strings.Contains(server, fmt.Sprintf("server_name %s ;", host)) | ||
}) | ||
|
||
f.WaitForNginxServer(host, | ||
func(server string) bool { | ||
return !strings.Contains(server, "# abc { }") | ||
}) | ||
|
||
f.HTTPTestClient(). | ||
GET("/"). | ||
WithHeader("Host", host). | ||
Expect(). | ||
Status(http.StatusNotFound) | ||
}) | ||
|
||
ginkgo.It("should drop an ingress if there is a forbidden word in some annotation", func() { | ||
host := "forbidden-value-test" | ||
|
||
annotations := map[string]string{ | ||
"nginx.ingress.kubernetes.io/configuration-snippet": ` | ||
default_type text/plain; | ||
content_by_lua_block { | ||
ngx.say("Hello World") | ||
}`, | ||
} | ||
|
||
ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) | ||
f.UpdateNginxConfigMapData("allow-snippet-annotations", "true") | ||
// Sleep a while just to guarantee that the configmap is applied | ||
framework.Sleep() | ||
f.EnsureIngress(ing) | ||
|
||
f.WaitForNginxServer(host, | ||
func(server string) bool { | ||
return !strings.Contains(server, fmt.Sprintf("server_name %s ;", host)) | ||
}) | ||
|
||
f.WaitForNginxServer(host, | ||
func(server string) bool { | ||
return !strings.Contains(server, `ngx.say("Hello World")`) | ||
}) | ||
|
||
f.HTTPTestClient(). | ||
GET("/"). | ||
WithHeader("Host", host). | ||
Expect(). | ||
Status(http.StatusNotFound) | ||
}) | ||
|
||
ginkgo.It("should drop an ingress if there is a custom blocklisted word in some annotation", func() { | ||
host := "custom-forbidden-value-test" | ||
|
||
annotations := map[string]string{ | ||
"nginx.ingress.kubernetes.io/configuration-snippet": ` | ||
# something_forbidden`, | ||
} | ||
|
||
ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) | ||
f.UpdateNginxConfigMapData("annotation-value-word-blocklist", "something_forbidden,otherthing_forbidden") | ||
// Sleep a while just to guarantee that the configmap is applied | ||
framework.Sleep() | ||
f.EnsureIngress(ing) | ||
|
||
f.WaitForNginxServer(host, | ||
func(server string) bool { | ||
return !strings.Contains(server, fmt.Sprintf("server_name %s ;", host)) | ||
}) | ||
|
||
f.WaitForNginxServer(host, | ||
func(server string) bool { | ||
return !strings.Contains(server, "# something_forbidden") | ||
}) | ||
|
||
f.HTTPTestClient(). | ||
GET("/"). | ||
WithHeader("Host", host). | ||
Expect(). | ||
Status(http.StatusNotFound) | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we add others like token etc?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you are fast uh! haha! Yeah, this list should be updated I guess. I tried to add the dangerous ones!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So what list do you think it's good to have here? When you say token, you say like the whole "/var/run/..." for the serviceaccount?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also add probably proxy_pass directives, so someone cannot bypass the internal balancer for the evil :D