Skip to content

Commit 273dd83

Browse files
committed
vpa/admission-controller: limit request payload size to 5MB
Add a defensive 5MB payload size cap using on the HTTP request body in the VPA Admission Controller webhook. This prevents arbitrary/maliciously large payload requests from exhausting memory resources and triggering Out-of-Memory (OOM) crashes (Denial of Service). The webhook continues to fail open on reading or unmarshaling errors to protect cluster scheduling availability.
1 parent 49f2277 commit 273dd83

2 files changed

Lines changed: 92 additions & 1 deletion

File tree

vertical-pod-autoscaler/pkg/admission-controller/logic/server.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ const (
4545
autoDeprecationWarning = `UpdateMode "Auto" is deprecated and will be removed in a future API version. ` +
4646
`Use explicit update modes like "Recreate", "Initial", or "InPlaceOrRecreate" instead. ` +
4747
`See https://github.com/kubernetes/autoscaler/issues/8424 for more details.`
48+
// maxAdmissionPayloadSize limits the size of the incoming admission request payload
49+
// to prevent OOM (Denial of Service) attacks. A typical AdmissionReview is well under 100KB,
50+
// and etcd limits objects to 1.5MB. With updates including both the new and old object,
51+
// 5MB is an extremely safe upper bound that leaves a comfortable margin.
52+
maxAdmissionPayloadSize = 1024 * 1024 * 5 // 5MB
4853
)
4954

5055
// AdmissionServer is an admission webhook server that modifies pod resources request based on VPA recommendation
@@ -192,8 +197,10 @@ func (s *AdmissionServer) Serve(w http.ResponseWriter, r *http.Request) {
192197

193198
var body []byte
194199
if r.Body != nil {
195-
if data, err := io.ReadAll(r.Body); err == nil {
200+
if data, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxAdmissionPayloadSize)); err == nil {
196201
body = data
202+
} else {
203+
klog.ErrorS(err, "Failed to read admission request body (payload may exceed 5MB limit)")
197204
}
198205
}
199206
// verify the content type is accurate
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
Copyright The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package logic
18+
19+
import (
20+
"bytes"
21+
"net/http"
22+
"net/http/httptest"
23+
"testing"
24+
25+
"github.com/stretchr/testify/assert"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
28+
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource"
29+
)
30+
31+
// InfiniteReader simulates an endless stream of bytes to verify LimitReader behavior.
32+
type InfiniteReader struct{}
33+
34+
func (r *InfiniteReader) Read(p []byte) (n int, err error) {
35+
for i := range p {
36+
p[i] = 'a'
37+
}
38+
return len(p), nil
39+
}
40+
41+
func (r *InfiniteReader) Close() error {
42+
return nil
43+
}
44+
45+
func TestServePayloadLimit(t *testing.T) {
46+
tests := []struct {
47+
name string
48+
requestBody *bytes.Buffer
49+
isEndless bool
50+
expectedStatus int
51+
}{
52+
{
53+
name: "Small valid JSON payload",
54+
requestBody: bytes.NewBufferString(`{"kind":"AdmissionReview","apiVersion":"admission.k8s.io/v1","request":{"uid":"123","resource":{"group":"autoscaling.k8s.io","version":"v1","resource":"verticalpodautoscalers"},"requestKind":{"group":"autoscaling.k8s.io","version":"v1","kind":"VerticalPodAutoscaler"}}}`),
55+
expectedStatus: http.StatusOK,
56+
},
57+
{
58+
name: "Oversized payload exceeding 5MB limit",
59+
isEndless: true,
60+
expectedStatus: http.StatusOK, // Fails open, returning 200 OK with unmarshal error rather than crashing
61+
},
62+
}
63+
64+
for _, tc := range tests {
65+
t.Run(tc.name, func(t *testing.T) {
66+
server := &AdmissionServer{
67+
resourceHandlers: make(map[metav1.GroupResource]resource.Handler), // Unused in basic HTTP parse step
68+
}
69+
70+
var req *http.Request
71+
if tc.isEndless {
72+
req = httptest.NewRequest(http.MethodPost, "/admit", &InfiniteReader{})
73+
} else {
74+
req = httptest.NewRequest(http.MethodPost, "/admit", tc.requestBody)
75+
}
76+
req.Header.Set("Content-Type", "application/json")
77+
78+
w := httptest.NewRecorder()
79+
server.Serve(w, req)
80+
81+
assert.Equal(t, tc.expectedStatus, w.Code)
82+
})
83+
}
84+
}

0 commit comments

Comments
 (0)