Skip to content

Commit 1b10bf8

Browse files
enjncdc
authored andcommitted
UPSTREAM: 121120: Prevent rapid reset http2 DOS on API server
This change fully addresses CVE-2023-44487 and CVE-2023-39325 for the API server when combined with golang/net@b225e7c The changes to util/runtime are required because otherwise a large number of requests can get blocked on the time.Sleep calls. For unauthenticated clients (either via 401 or the anonymous user), we simply no longer allow such clients to hold open http2 connections. They can use http2, but with the performance of http1 (or possibly slightly worse). For all other clients, we detect if the request ended via a timeout before the context's deadline. This likely means that the client reset the http2 stream early. We close the connection in this case as well. To mitigate issues related to clients creating more streams than the configured max, we rely on the golang.org/x/net fix noted above. The Kube API server now uses a max stream of 100 instead of 250 (this matches the Go http2 client default). This lowers the abuse limit from 1000 to 400. Signed-off-by: Monis Khan <[email protected]> Signed-off-by: Andy Goldstein <[email protected]>
1 parent 38a2198 commit 1b10bf8

File tree

5 files changed

+71
-11
lines changed

5 files changed

+71
-11
lines changed

staging/src/k8s.io/apimachinery/pkg/util/runtime/runtime.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,17 @@ type rudimentaryErrorBackoff struct {
126126
// OnError will block if it is called more often than the embedded period time.
127127
// This will prevent overly tight hot error loops.
128128
func (r *rudimentaryErrorBackoff) OnError(error) {
129+
now := time.Now() // start the timer before acquiring the lock
129130
r.lastErrorTimeLock.Lock()
130-
defer r.lastErrorTimeLock.Unlock()
131-
d := time.Since(r.lastErrorTime)
132-
if d < r.minPeriod {
133-
// If the time moves backwards for any reason, do nothing
134-
time.Sleep(r.minPeriod - d)
135-
}
131+
d := now.Sub(r.lastErrorTime)
136132
r.lastErrorTime = time.Now()
133+
r.lastErrorTimeLock.Unlock()
134+
135+
// Do not sleep with the lock held because that causes all callers of HandleError to block.
136+
// We only want the current goroutine to block.
137+
// A negative or zero duration causes time.Sleep to return immediately.
138+
// If the time moves backwards for any reason, do nothing.
139+
time.Sleep(r.minPeriod - d)
137140
}
138141

139142
// GetCaller returns the caller of the function that calls it.

staging/src/k8s.io/apiserver/pkg/endpoints/filters/authentication.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"k8s.io/apiserver/pkg/authentication/authenticator"
3030
"k8s.io/apiserver/pkg/authentication/authenticatorfactory"
3131
"k8s.io/apiserver/pkg/authentication/request/headerrequest"
32+
"k8s.io/apiserver/pkg/authentication/user"
3233
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
3334
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
3435
"k8s.io/klog/v2"
@@ -101,13 +102,28 @@ func withAuthentication(handler http.Handler, auth authenticator.Request, failed
101102
)
102103
}
103104

105+
// http2 is an expensive protocol that is prone to abuse,
106+
// see CVE-2023-44487 and CVE-2023-39325 for an example.
107+
// Do not allow unauthenticated clients to keep these
108+
// connections open (i.e. basically degrade them to http1).
109+
if req.ProtoMajor == 2 && isAnonymousUser(resp.User) {
110+
w.Header().Set("Connection", "close")
111+
}
112+
104113
req = req.WithContext(genericapirequest.WithUser(req.Context(), resp.User))
105114
handler.ServeHTTP(w, req)
106115
})
107116
}
108117

109118
func Unauthorized(s runtime.NegotiatedSerializer) http.Handler {
110119
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
120+
// http2 is an expensive protocol that is prone to abuse,
121+
// see CVE-2023-44487 and CVE-2023-39325 for an example.
122+
// Do not allow unauthenticated clients to keep these
123+
// connections open (i.e. basically degrade them to http1).
124+
if req.ProtoMajor == 2 {
125+
w.Header().Set("Connection", "close")
126+
}
111127
ctx := req.Context()
112128
requestInfo, found := genericapirequest.RequestInfoFrom(ctx)
113129
if !found {
@@ -127,3 +143,15 @@ func audiencesAreAcceptable(apiAuds, responseAudiences authenticator.Audiences)
127143

128144
return len(apiAuds.Intersect(responseAudiences)) > 0
129145
}
146+
147+
func isAnonymousUser(u user.Info) bool {
148+
if u.GetName() == user.Anonymous {
149+
return true
150+
}
151+
for _, group := range u.GetGroups() {
152+
if group == user.AllUnauthenticated {
153+
return true
154+
}
155+
}
156+
return false
157+
}

staging/src/k8s.io/apiserver/pkg/server/filters/goaway.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ type goaway struct {
6464

6565
// ServeHTTP implement HTTP handler
6666
func (h *goaway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
67-
if r.Proto == "HTTP/2.0" && h.decider.Goaway(r) {
67+
if r.ProtoMajor == 2 && h.decider.Goaway(r) {
6868
// Send a GOAWAY and tear down the TCP connection when idle.
6969
w.Header().Set("Connection", "close")
7070
}

staging/src/k8s.io/apiserver/pkg/server/filters/timeout.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,13 @@ func (t *timeoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
143143
}()
144144

145145
defer postTimeoutFn()
146-
tw.timeout(err)
146+
tw.timeout(r, err)
147147
}
148148
}
149149

150150
type timeoutWriter interface {
151151
http.ResponseWriter
152-
timeout(*apierrors.StatusError)
152+
timeout(*http.Request, *apierrors.StatusError)
153153
}
154154

155155
func newTimeoutWriter(w http.ResponseWriter) (timeoutWriter, http.ResponseWriter) {
@@ -242,7 +242,7 @@ func copyHeaders(dst, src http.Header) {
242242
}
243243
}
244244

245-
func (tw *baseTimeoutWriter) timeout(err *apierrors.StatusError) {
245+
func (tw *baseTimeoutWriter) timeout(r *http.Request, err *apierrors.StatusError) {
246246
tw.mu.Lock()
247247
defer tw.mu.Unlock()
248248

@@ -252,6 +252,14 @@ func (tw *baseTimeoutWriter) timeout(err *apierrors.StatusError) {
252252
// We can safely timeout the HTTP request by sending by a timeout
253253
// handler
254254
if !tw.wroteHeader && !tw.hijacked {
255+
// http2 is an expensive protocol that is prone to abuse,
256+
// see CVE-2023-44487 and CVE-2023-39325 for an example.
257+
// Do not allow clients to reset these connections
258+
// prematurely as that can trivially OOM the api server
259+
// (i.e. basically degrade them to http1).
260+
if isLikelyEarlyHTTP2Reset(r) {
261+
tw.w.Header().Set("Connection", "close")
262+
}
255263
tw.w.WriteHeader(http.StatusGatewayTimeout)
256264
enc := json.NewEncoder(tw.w)
257265
enc.Encode(&err.ErrStatus)
@@ -274,6 +282,24 @@ func (tw *baseTimeoutWriter) timeout(err *apierrors.StatusError) {
274282
}
275283
}
276284

285+
// isLikelyEarlyHTTP2Reset returns true if an http2 stream was reset before the request deadline.
286+
// Note that this does not prevent a client from trying to create more streams than the configured
287+
// max, but https://github.com/golang/net/commit/b225e7ca6dde1ef5a5ae5ce922861bda011cfabd protects
288+
// us from abuse via that vector.
289+
func isLikelyEarlyHTTP2Reset(r *http.Request) bool {
290+
if r.ProtoMajor != 2 {
291+
return false
292+
}
293+
294+
deadline, ok := r.Context().Deadline()
295+
if !ok {
296+
return true // this context had no deadline but was canceled meaning the client likely reset it early
297+
}
298+
299+
// this context was canceled before its deadline meaning the client likely reset it early
300+
return time.Now().Before(deadline)
301+
}
302+
277303
func (tw *baseTimeoutWriter) CloseNotify() <-chan bool {
278304
tw.mu.Lock()
279305
defer tw.mu.Unlock()

staging/src/k8s.io/apiserver/pkg/server/secure_serving.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,10 @@ func (s *SecureServingInfo) Serve(handler http.Handler, shutdownTimeout time.Dur
189189
if s.HTTP2MaxStreamsPerConnection > 0 {
190190
http2Options.MaxConcurrentStreams = uint32(s.HTTP2MaxStreamsPerConnection)
191191
} else {
192-
http2Options.MaxConcurrentStreams = 250
192+
// match http2.initialMaxConcurrentStreams used by clients
193+
// this makes it so that a malicious client can only open 400 streams before we forcibly close the connection
194+
// https://github.com/golang/net/commit/b225e7ca6dde1ef5a5ae5ce922861bda011cfabd
195+
http2Options.MaxConcurrentStreams = 100
193196
}
194197

195198
// increase the connection buffer size from the 1MB default to handle the specified number of concurrent streams

0 commit comments

Comments
 (0)