From eadc9b65ed5954043004218fa884fc59f6dd68df Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani <45800463+rchincha@users.noreply.github.com> Date: Sat, 18 Apr 2026 10:39:08 -0700 Subject: [PATCH] fix(security): limit API key creation body to 4 KiB (INPUT-2) (#3978) Wrap req.Body with http.MaxBytesReader before io.ReadAll in CreateAPIKey. Requests with bodies larger than MaxAPIKeyBodySize (4 KiB) now return HTTP 413 instead of buffering arbitrary data. Add the MaxAPIKeyBodySize constant, update the Swagger @Failure annotation to document 413, and add a unit test. Signed-off-by: Ramkumar Chinchani --- pkg/api/constants/consts.go | 4 +++- pkg/api/routes.go | 12 +++++++++--- pkg/api/routes_test.go | 16 ++++++++++++++++ swagger/docs.go | 6 ++++++ swagger/swagger.json | 6 ++++++ swagger/swagger.yaml | 4 ++++ 6 files changed, 44 insertions(+), 4 deletions(-) diff --git a/pkg/api/constants/consts.go b/pkg/api/constants/consts.go index 93c00883..37660942 100644 --- a/pkg/api/constants/consts.go +++ b/pkg/api/constants/consts.go @@ -22,7 +22,9 @@ const ( MaxManifestDigestQueryTags = (8192 - 2048) / (len("tag=") + 128 + 1) // MaxManifestBodySize is the maximum number of bytes accepted for a manifest PUT request body. // OCI manifest JSON is always small metadata; 4 MiB is well above any realistic manifest. - MaxManifestBodySize = 4 * 1024 * 1024 + MaxManifestBodySize = 4 * 1024 * 1024 + // MaxAPIKeyBodySize is the maximum number of bytes accepted for an API-key creation request body. + MaxAPIKeyBodySize = 4 * 1024 BlobUploadUUID = "Blob-Upload-UUID" DefaultMediaType = "application/json" BinaryMediaType = "application/octet-stream" diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 5c2e431f..c86e840c 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -2342,15 +2342,21 @@ func (rh *RouteHandler) GetAPIKeys(resp http.ResponseWriter, req *http.Request) // @Success 201 {string} string "created" // @Failure 400 {string} string "bad request" // @Failure 401 {string} string "unauthorized" +// @Failure 413 {string} string "request entity too large" // @Failure 500 {string} string "internal server error" // @Router /zot/auth/apikey [post]. func (rh *RouteHandler) CreateAPIKey(resp http.ResponseWriter, req *http.Request) { var payload APIKeyPayload - body, err := io.ReadAll(req.Body) + body, err := io.ReadAll(http.MaxBytesReader(resp, req.Body, constants.MaxAPIKeyBodySize)) if err != nil { - rh.c.Log.Error().Msg("failed to read request body") - resp.WriteHeader(http.StatusInternalServerError) + var mbe *http.MaxBytesError + if errors.As(err, &mbe) { + resp.WriteHeader(http.StatusRequestEntityTooLarge) + } else { + rh.c.Log.Error().Msg("failed to read request body") + resp.WriteHeader(http.StatusInternalServerError) + } return } diff --git a/pkg/api/routes_test.go b/pkg/api/routes_test.go index d65af6fe..2d706cd7 100644 --- a/pkg/api/routes_test.go +++ b/pkg/api/routes_test.go @@ -1604,6 +1604,22 @@ func TestRoutes(t *testing.T) { So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError) }) + Convey("CreateAPIKey body exceeds MaxAPIKeyBodySize returns 413", func() { + userAc := reqCtx.NewUserAccessControl() + userAc.SetUsername("test") + ctx := userAc.DeriveContext(context.Background()) + + oversized := make([]byte, constants.MaxAPIKeyBodySize+1) + request, _ := http.NewRequestWithContext(ctx, http.MethodPost, baseURL, bytes.NewReader(oversized)) + response := httptest.NewRecorder() + + rthdlr.CreateAPIKey(response, request) + + resp := response.Result() + defer resp.Body.Close() + So(resp.StatusCode, ShouldEqual, http.StatusRequestEntityTooLarge) + }) + Convey("CreateAPIKey bad request body", func() { userAc := reqCtx.NewUserAccessControl() userAc.SetUsername("test") diff --git a/swagger/docs.go b/swagger/docs.go index af166c0b..518e8535 100644 --- a/swagger/docs.go +++ b/swagger/docs.go @@ -1099,6 +1099,12 @@ const docTemplate = `{ "type": "string" } }, + "413": { + "description": "request entity too large", + "schema": { + "type": "string" + } + }, "500": { "description": "internal server error", "schema": { diff --git a/swagger/swagger.json b/swagger/swagger.json index 15077763..87f9ce57 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -1091,6 +1091,12 @@ "type": "string" } }, + "413": { + "description": "request entity too large", + "schema": { + "type": "string" + } + }, "500": { "description": "internal server error", "schema": { diff --git a/swagger/swagger.yaml b/swagger/swagger.yaml index 6bffaab4..f1b41a9b 100644 --- a/swagger/swagger.yaml +++ b/swagger/swagger.yaml @@ -1004,6 +1004,10 @@ paths: description: unauthorized schema: type: string + "413": + description: request entity too large + schema: + type: string "500": description: internal server error schema: