From caeccdceff6e2d3cb1a9b05f8efe33308b178bf9 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 19 Apr 2026 21:39:08 +0200 Subject: [PATCH] prevent out-of-memory errors (#5674) impose a maximum size on body of incoming HTTP requests and responses. --- internal/api/api.go | 4 ++++ internal/api/api_config_global.go | 3 ++- internal/api/api_config_pathdefaults.go | 3 ++- internal/api/api_config_paths.go | 7 ++++--- internal/auth/manager.go | 8 +++++--- internal/protocols/whip/client.go | 6 +++++- internal/servers/hls/hlsjsdownloader/main.go | 6 +++++- internal/servers/webrtc/http_server.go | 4 ++-- internal/servers/webrtc/server.go | 5 +++-- .../staticsources/rpicamera/mtxrpicamdownloader/main.go | 6 +++++- 10 files changed, 37 insertions(+), 15 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index 6c7d5f85..512b04dd 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -19,6 +19,10 @@ import ( "github.com/bluenviron/mediamtx/internal/protocols/httpp" ) +const ( + maxInboundConfigSize = 10 * 1024 * 1024 +) + func interfaceIsEmpty(i any) bool { return reflect.ValueOf(i).Kind() != reflect.Pointer || reflect.ValueOf(i).IsNil() } diff --git a/internal/api/api_config_global.go b/internal/api/api_config_global.go index 8d26ffb9..b43e0fa3 100644 --- a/internal/api/api_config_global.go +++ b/internal/api/api_config_global.go @@ -1,6 +1,7 @@ package api //nolint:revive import ( + "io" "net/http" "github.com/bluenviron/mediamtx/internal/conf" @@ -18,7 +19,7 @@ func (a *API) onConfigGlobalGet(ctx *gin.Context) { func (a *API) onConfigGlobalPatch(ctx *gin.Context) { var c conf.OptionalGlobal - err := jsonwrapper.Decode(ctx.Request.Body, &c) + err := jsonwrapper.Decode(io.LimitReader(ctx.Request.Body, maxInboundConfigSize), &c) if err != nil { a.writeError(ctx, http.StatusBadRequest, err) return diff --git a/internal/api/api_config_pathdefaults.go b/internal/api/api_config_pathdefaults.go index f53597a9..d0675720 100644 --- a/internal/api/api_config_pathdefaults.go +++ b/internal/api/api_config_pathdefaults.go @@ -1,6 +1,7 @@ package api //nolint:revive import ( + "io" "net/http" "github.com/bluenviron/mediamtx/internal/conf" @@ -18,7 +19,7 @@ func (a *API) onConfigPathDefaultsGet(ctx *gin.Context) { func (a *API) onConfigPathDefaultsPatch(ctx *gin.Context) { var p conf.OptionalPath - err := jsonwrapper.Decode(ctx.Request.Body, &p) + err := jsonwrapper.Decode(io.LimitReader(ctx.Request.Body, maxInboundConfigSize), &p) if err != nil { a.writeError(ctx, http.StatusBadRequest, err) return diff --git a/internal/api/api_config_paths.go b/internal/api/api_config_paths.go index 01b7a2bd..6ac5b9e2 100644 --- a/internal/api/api_config_paths.go +++ b/internal/api/api_config_paths.go @@ -3,6 +3,7 @@ package api //nolint:revive import ( "errors" "fmt" + "io" "net/http" "github.com/bluenviron/mediamtx/internal/conf" @@ -63,7 +64,7 @@ func (a *API) onConfigPathsAdd(ctx *gin.Context) { //nolint:dupl } var p conf.OptionalPath - err := jsonwrapper.Decode(ctx.Request.Body, &p) + err := jsonwrapper.Decode(io.LimitReader(ctx.Request.Body, maxInboundConfigSize), &p) if err != nil { a.writeError(ctx, http.StatusBadRequest, err) return @@ -100,7 +101,7 @@ func (a *API) onConfigPathsPatch(ctx *gin.Context) { //nolint:dupl } var p conf.OptionalPath - err := jsonwrapper.Decode(ctx.Request.Body, &p) + err := jsonwrapper.Decode(io.LimitReader(ctx.Request.Body, maxInboundConfigSize), &p) if err != nil { a.writeError(ctx, http.StatusBadRequest, err) return @@ -141,7 +142,7 @@ func (a *API) onConfigPathsReplace(ctx *gin.Context) { //nolint:dupl } var p conf.OptionalPath - err := jsonwrapper.Decode(ctx.Request.Body, &p) + err := jsonwrapper.Decode(io.LimitReader(ctx.Request.Body, maxInboundConfigSize), &p) if err != nil { a.writeError(ctx, http.StatusBadRequest, err) return diff --git a/internal/auth/manager.go b/internal/auth/manager.go index aef19d5d..938dcc64 100644 --- a/internal/auth/manager.go +++ b/internal/auth/manager.go @@ -24,7 +24,8 @@ const ( // PauseAfterError is the pause to apply after an authentication failure. PauseAfterError = 2 * time.Second - jwksRefreshPeriod = 60 * 60 * time.Second + maxInboundBodySize = 128 * 1024 + jwksRefreshPeriod = 60 * 60 * time.Second ) func isHTTP(req *Request) bool { @@ -234,7 +235,8 @@ func (m *Manager) authenticateHTTP(req *Request, token string) (string, error) { defer res.Body.Close() if res.StatusCode < 200 || res.StatusCode > 299 { - if resBody, err2 := io.ReadAll(res.Body); err2 == nil && len(resBody) != 0 { + resBody, err2 := io.ReadAll(io.LimitReader(res.Body, maxInboundBodySize)) + if err2 == nil && len(resBody) != 0 { return "", fmt.Errorf("server replied with code %d: %s", res.StatusCode, string(resBody)) } @@ -304,7 +306,7 @@ func (m *Manager) pullJWTJWKS() (jwt.Keyfunc, error) { defer res.Body.Close() var raw json.RawMessage - err = json.NewDecoder(res.Body).Decode(&raw) + err = json.NewDecoder(io.LimitReader(res.Body, maxInboundBodySize)).Decode(&raw) if err != nil { return nil, err } diff --git a/internal/protocols/whip/client.go b/internal/protocols/whip/client.go index 1e28f185..f978a38b 100644 --- a/internal/protocols/whip/client.go +++ b/internal/protocols/whip/client.go @@ -18,6 +18,10 @@ import ( "github.com/bluenviron/mediamtx/internal/protocols/webrtc" ) +const ( + maxInboundSDPSize = 128 * 1024 +) + // Client is a WHIP client. type Client struct { URL *url.URL @@ -260,7 +264,7 @@ func (c *Client) postOffer( return nil, fmt.Errorf("ETag is missing") } - sdp, err := io.ReadAll(res.Body) + sdp, err := io.ReadAll(io.LimitReader(res.Body, maxInboundSDPSize)) if err != nil { return nil, err } diff --git a/internal/servers/hls/hlsjsdownloader/main.go b/internal/servers/hls/hlsjsdownloader/main.go index 020e682d..1e332ba5 100644 --- a/internal/servers/hls/hlsjsdownloader/main.go +++ b/internal/servers/hls/hlsjsdownloader/main.go @@ -15,6 +15,10 @@ import ( "strings" ) +const ( + maxInboundHLSJSSize = 10 * 1024 * 1024 +) + func do() error { buf, err := os.ReadFile("./hlsjsdownloader/VERSION") if err != nil { @@ -34,7 +38,7 @@ func do() error { return fmt.Errorf("bad status code: %v", res.StatusCode) } - zipBuf, err := io.ReadAll(res.Body) + zipBuf, err := io.ReadAll(io.LimitReader(res.Body, maxInboundHLSJSSize)) if err != nil { return err } diff --git a/internal/servers/webrtc/http_server.go b/internal/servers/webrtc/http_server.go index 3c5b503d..f78d5047 100644 --- a/internal/servers/webrtc/http_server.go +++ b/internal/servers/webrtc/http_server.go @@ -194,7 +194,7 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool) return } - offer, err := io.ReadAll(ctx.Request.Body) + offer, err := io.ReadAll(io.LimitReader(ctx.Request.Body, maxInboundSDPSize)) if err != nil { return } @@ -260,7 +260,7 @@ func (s *httpServer) onWHIPPatch(ctx *gin.Context, pathName string, rawSecret st return } - byts, err := io.ReadAll(ctx.Request.Body) + byts, err := io.ReadAll(io.LimitReader(ctx.Request.Body, maxInboundSDPSize)) if err != nil { return } diff --git a/internal/servers/webrtc/server.go b/internal/servers/webrtc/server.go index d11ea876..1c40bc32 100644 --- a/internal/servers/webrtc/server.go +++ b/internal/servers/webrtc/server.go @@ -32,7 +32,8 @@ import ( ) const ( - webrtcTurnSecretExpiration = 24 * time.Hour + turnSecretExpiration = 24 * time.Hour + maxInboundSDPSize = 128 * 1024 ) // ErrSessionNotFound is returned when a session is not found. @@ -475,7 +476,7 @@ func (s *Server) generateICEServers(clientConfig bool) ([]pwebrtc.ICEServer, err for _, server := range s.ICEServers { if !server.ClientOnly || clientConfig { if server.Username == "AUTH_SECRET" { - expireDate := time.Now().Add(webrtcTurnSecretExpiration).Unix() + expireDate := time.Now().Add(turnSecretExpiration).Unix() user, err := randomTurnUser() if err != nil { diff --git a/internal/staticsources/rpicamera/mtxrpicamdownloader/main.go b/internal/staticsources/rpicamera/mtxrpicamdownloader/main.go index 412e7942..dea9ee05 100644 --- a/internal/staticsources/rpicamera/mtxrpicamdownloader/main.go +++ b/internal/staticsources/rpicamera/mtxrpicamdownloader/main.go @@ -16,6 +16,10 @@ import ( "strings" ) +const ( + maxInboundRPICameraSize = 10 * 1024 * 1024 +) + func dumpTar(src io.Reader) error { uncompressed, err := gzip.NewReader(src) if err != nil { @@ -75,7 +79,7 @@ func doSingle(version string, f string) error { return fmt.Errorf("bad status code: %v", res.StatusCode) } - buf, err := io.ReadAll(res.Body) + buf, err := io.ReadAll(io.LimitReader(res.Body, maxInboundRPICameraSize)) if err != nil { return err }