Skip to content

Commit 56ee9e9

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 270de19 commit 56ee9e9

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
@@ -146,13 +146,13 @@ func (t *timeoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
146146
return false
147147
})
148148
defer postTimeoutFn()
149-
tw.timeout(err)
149+
tw.timeout(r, err)
150150
}
151151
}
152152

153153
type timeoutWriter interface {
154154
http.ResponseWriter
155-
timeout(*apierrors.StatusError)
155+
timeout(*http.Request, *apierrors.StatusError)
156156
}
157157

158158
func newTimeoutWriter(w http.ResponseWriter) (timeoutWriter, http.ResponseWriter) {
@@ -245,7 +245,7 @@ func copyHeaders(dst, src http.Header) {
245245
}
246246
}
247247

248-
func (tw *baseTimeoutWriter) timeout(err *apierrors.StatusError) {
248+
func (tw *baseTimeoutWriter) timeout(r *http.Request, err *apierrors.StatusError) {
249249
tw.mu.Lock()
250250
defer tw.mu.Unlock()
251251

@@ -255,6 +255,14 @@ func (tw *baseTimeoutWriter) timeout(err *apierrors.StatusError) {
255255
// We can safely timeout the HTTP request by sending by a timeout
256256
// handler
257257
if !tw.wroteHeader && !tw.hijacked {
258+
// http2 is an expensive protocol that is prone to abuse,
259+
// see CVE-2023-44487 and CVE-2023-39325 for an example.
260+
// Do not allow clients to reset these connections
261+
// prematurely as that can trivially OOM the api server
262+
// (i.e. basically degrade them to http1).
263+
if isLikelyEarlyHTTP2Reset(r) {
264+
tw.w.Header().Set("Connection", "close")
265+
}
258266
tw.w.WriteHeader(http.StatusGatewayTimeout)
259267
enc := json.NewEncoder(tw.w)
260268
enc.Encode(&err.ErrStatus)
@@ -277,6 +285,24 @@ func (tw *baseTimeoutWriter) timeout(err *apierrors.StatusError) {
277285
}
278286
}
279287

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