Skip to content

Commit 69e14f1

Browse files
authored
⚠️ Use limited reader in webhooks (#2598)
* add missing limitedReader Signed-off-by: Tim Ramlot <[email protected]> * update maxRequestSize and explain values in comment Signed-off-by: Tim Ramlot <[email protected]> * use more specific http error code Signed-off-by: Tim Ramlot <[email protected]> * add extra comments and update value for maxRequestSize Signed-off-by: Tim Ramlot <[email protected]> * use const instead of var Signed-off-by: Tim Ramlot <[email protected]> --------- Signed-off-by: Tim Ramlot <[email protected]>
1 parent 5c21730 commit 69e14f1

File tree

4 files changed

+71
-2
lines changed

4 files changed

+71
-2
lines changed

pkg/webhook/admission/http.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,26 @@ import (
3434
var admissionScheme = runtime.NewScheme()
3535
var admissionCodecs = serializer.NewCodecFactory(admissionScheme)
3636

37+
// adapted from https://github.com/kubernetes/kubernetes/blob/c28c2009181fcc44c5f6b47e10e62dacf53e4da0/staging/src/k8s.io/pod-security-admission/cmd/webhook/server/server.go
38+
//
39+
// From https://github.com/kubernetes/apiserver/blob/d6876a0600de06fef75968c4641c64d7da499f25/pkg/server/config.go#L433-L442C5:
40+
//
41+
// 1.5MB is the recommended client request size in byte
42+
// the etcd server should accept. See
43+
// https://github.com/etcd-io/etcd/blob/release-3.4/embed/config.go#L56.
44+
// A request body might be encoded in json, and is converted to
45+
// proto when persisted in etcd, so we allow 2x as the largest request
46+
// body size to be accepted and decoded in a write request.
47+
//
48+
// For the admission request, we can infer that it contains at most two objects
49+
// (the old and new versions of the object being admitted), each of which can
50+
// be at most 3MB in size. For the rest of the request, we can assume that
51+
// it will be less than 1MB in size. Therefore, we can set the max request
52+
// size to 7MB.
53+
// If your use case requires larger max request sizes, please
54+
// open an issue (https://github.com/kubernetes-sigs/controller-runtime/issues/new).
55+
const maxRequestSize = int64(7 * 1024 * 1024)
56+
3757
func init() {
3858
utilruntime.Must(v1.AddToScheme(admissionScheme))
3959
utilruntime.Must(v1beta1.AddToScheme(admissionScheme))
@@ -55,12 +75,19 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
5575
}
5676

5777
defer r.Body.Close()
58-
body, err := io.ReadAll(r.Body)
78+
limitedReader := &io.LimitedReader{R: r.Body, N: maxRequestSize}
79+
body, err := io.ReadAll(limitedReader)
5980
if err != nil {
6081
wh.getLogger(nil).Error(err, "unable to read the body from the incoming request")
6182
wh.writeResponse(w, Errored(http.StatusBadRequest, err))
6283
return
6384
}
85+
if limitedReader.N <= 0 {
86+
err := fmt.Errorf("request entity is too large; limit is %d bytes", maxRequestSize)
87+
wh.getLogger(nil).Error(err, "unable to read the body from the incoming request; limit reached")
88+
wh.writeResponse(w, Errored(http.StatusRequestEntityTooLarge, err))
89+
return
90+
}
6491

6592
// verify the content type is accurate
6693
if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {

pkg/webhook/admission/http_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package admission
1919
import (
2020
"bytes"
2121
"context"
22+
"crypto/rand"
2223
"fmt"
2324
"io"
2425
"net/http"
@@ -97,6 +98,19 @@ var _ = Describe("Admission Webhooks", func() {
9798
Expect(respRecorder.Body.String()).To(Equal(expected))
9899
})
99100

101+
It("should error when given an infinite body", func() {
102+
req := &http.Request{
103+
Header: http.Header{"Content-Type": []string{"application/json"}},
104+
Method: http.MethodPost,
105+
Body: nopCloser{Reader: rand.Reader},
106+
}
107+
108+
expected := `{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"request entity is too large; limit is 7340032 bytes","code":413}}}
109+
`
110+
webhook.ServeHTTP(respRecorder, req)
111+
Expect(respRecorder.Body.String()).To(Equal(expected))
112+
})
113+
100114
It("should return the response given by the handler with version defaulted to v1", func() {
101115
req := &http.Request{
102116
Header: http.Header{"Content-Type": []string{"application/json"}},

pkg/webhook/authentication/http.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ import (
3434
var authenticationScheme = runtime.NewScheme()
3535
var authenticationCodecs = serializer.NewCodecFactory(authenticationScheme)
3636

37+
// The TokenReview resource mostly contains a bearer token which
38+
// at most should have a few KB's of size, so we picked 1 MB to
39+
// have plenty of buffer.
40+
// If your use case requires larger max request sizes, please
41+
// open an issue (https://github.com/kubernetes-sigs/controller-runtime/issues/new).
42+
const maxRequestSize = int64(1 * 1024 * 1024)
43+
3744
func init() {
3845
utilruntime.Must(authenticationv1.AddToScheme(authenticationScheme))
3946
utilruntime.Must(authenticationv1beta1.AddToScheme(authenticationScheme))
@@ -55,12 +62,19 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
5562
}
5663

5764
defer r.Body.Close()
58-
body, err := io.ReadAll(r.Body)
65+
limitedReader := &io.LimitedReader{R: r.Body, N: maxRequestSize}
66+
body, err := io.ReadAll(limitedReader)
5967
if err != nil {
6068
wh.getLogger(nil).Error(err, "unable to read the body from the incoming request")
6169
wh.writeResponse(w, Errored(err))
6270
return
6371
}
72+
if limitedReader.N <= 0 {
73+
err := fmt.Errorf("request entity is too large; limit is %d bytes", maxRequestSize)
74+
wh.getLogger(nil).Error(err, "unable to read the body from the incoming request; limit reached")
75+
wh.writeResponse(w, Errored(err))
76+
return
77+
}
6478

6579
// verify the content type is accurate
6680
if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {

pkg/webhook/authentication/http_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package authentication
1919
import (
2020
"bytes"
2121
"context"
22+
"crypto/rand"
2223
"fmt"
2324
"io"
2425
"net/http"
@@ -107,6 +108,19 @@ var _ = Describe("Authentication Webhooks", func() {
107108
Expect(respRecorder.Body.String()).To(Equal(expected))
108109
})
109110

111+
It("should error when given an infinite body", func() {
112+
req := &http.Request{
113+
Header: http.Header{"Content-Type": []string{"application/json"}},
114+
Method: http.MethodPost,
115+
Body: nopCloser{Reader: rand.Reader},
116+
}
117+
118+
expected := `{"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},"error":"request entity is too large; limit is 1048576 bytes"}}
119+
`
120+
webhook.ServeHTTP(respRecorder, req)
121+
Expect(respRecorder.Body.String()).To(Equal(expected))
122+
})
123+
110124
It("should return the response given by the handler with version defaulted to v1", func() {
111125
req := &http.Request{
112126
Header: http.Header{"Content-Type": []string{"application/json"}},

0 commit comments

Comments
 (0)