From e9c274c2115b109af04a152cbb25fd41ff37c9d9 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 1 Apr 2026 19:49:43 +0200 Subject: [PATCH 1/6] Fix NuGet package upload error handling and `UploadStream` nil pointer Fix `UploadStream()` to guard against nil `MultipartForm` which can occur when `ParseMultipartForm` silently returns nil on malformed Content-Type boundaries (e.g. unquoted boundaries containing `=`). Also remove the `application/x-www-form-urlencoded` branch which would always fail for multipart parsing. Wrap `zip.NewReader` errors in NuGet `ParsePackageMetaData` and `ExtractPortablePdb` as `ErrInvalidArgument` so invalid packages return HTTP 400 instead of 500. Add integration test for multipart/form-data NuGet upload path (used by `dotnet nuget push`) which was previously untested. Fixes https://github.com/go-gitea/gitea/issues/36932 Co-Authored-By: Claude (Opus 4.6) --- modules/packages/nuget/metadata.go | 2 +- modules/packages/nuget/symbol_extractor.go | 2 +- services/context/context_request.go | 15 +++++------ tests/integration/api_packages_nuget_test.go | 27 ++++++++++++++++++++ 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go index 51246273957e5..c3b708f403ef2 100644 --- a/modules/packages/nuget/metadata.go +++ b/modules/packages/nuget/metadata.go @@ -140,7 +140,7 @@ type nuspecPackage struct { func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) { archive, err := zip.NewReader(r, size) if err != nil { - return nil, err + return nil, util.NewInvalidArgumentErrorf("not a valid NuGet package: %v", err) } for _, file := range archive.File { diff --git a/modules/packages/nuget/symbol_extractor.go b/modules/packages/nuget/symbol_extractor.go index 2eadee5463a27..f37a36a608dd2 100644 --- a/modules/packages/nuget/symbol_extractor.go +++ b/modules/packages/nuget/symbol_extractor.go @@ -42,7 +42,7 @@ func (l PortablePdbList) Close() { func ExtractPortablePdb(r io.ReaderAt, size int64) (PortablePdbList, error) { archive, err := zip.NewReader(r, size) if err != nil { - return nil, err + return nil, util.NewInvalidArgumentErrorf("not a valid symbol package: %v", err) } var pdbs PortablePdbList diff --git a/services/context/context_request.go b/services/context/context_request.go index 984b9ac793e8d..86a20c5e67395 100644 --- a/services/context/context_request.go +++ b/services/context/context_request.go @@ -13,17 +13,16 @@ import ( // Only form files need to get closed. func (ctx *Context) UploadStream() (rd io.ReadCloser, needToClose bool, err error) { contentType := strings.ToLower(ctx.Req.Header.Get("Content-Type")) - if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") || strings.HasPrefix(contentType, "multipart/form-data") { + if strings.HasPrefix(contentType, "multipart/form-data") { if err := ctx.Req.ParseMultipartForm(32 << 20); err != nil { return nil, false, err } - if ctx.Req.MultipartForm.File == nil { - return nil, false, http.ErrMissingFile - } - for _, files := range ctx.Req.MultipartForm.File { - if len(files) > 0 { - r, err := files[0].Open() - return r, true, err + if ctx.Req.MultipartForm != nil { + for _, files := range ctx.Req.MultipartForm.File { + if len(files) > 0 { + r, err := files[0].Open() + return r, true, err + } } } return nil, false, http.ErrMissingFile diff --git a/tests/integration/api_packages_nuget_test.go b/tests/integration/api_packages_nuget_test.go index f29a43f98043f..2314ab45d6163 100644 --- a/tests/integration/api_packages_nuget_test.go +++ b/tests/integration/api_packages_nuget_test.go @@ -10,6 +10,7 @@ import ( "encoding/xml" "fmt" "io" + "mime/multipart" "net/http" "net/http/httptest" neturl "net/url" @@ -930,4 +931,30 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusNotFound) }) + + t.Run("UploadMultipartForm", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + packageContent := createPackage(packageName, packageVersion).Bytes() + + // Simulate dotnet nuget push which sends multipart/form-data + var body bytes.Buffer + mpw := multipart.NewWriter(&body) + part, err := mpw.CreateFormFile("package", "package.nupkg") + assert.NoError(t, err) + _, err = part.Write(packageContent) + assert.NoError(t, err) + err = mpw.Close() + assert.NoError(t, err) + + req := NewRequestWithBody(t, "PUT", url, &body). + AddBasicAuth(user.Name). + SetHeader("Content-Type", mpw.FormDataContentType()) + MakeRequest(t, req, http.StatusCreated) + + // Clean up + req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, packageName, packageVersion)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNoContent) + }) } From 86344111d2514f65eabef0327408c2ea13e3cd04 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 1 Apr 2026 19:52:11 +0200 Subject: [PATCH 2/6] Fix "symbol package" to "symbols package" to match NuGet terminology Co-Authored-By: Claude (Opus 4.6) --- modules/packages/nuget/symbol_extractor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/packages/nuget/symbol_extractor.go b/modules/packages/nuget/symbol_extractor.go index f37a36a608dd2..21d45a0b4e8e8 100644 --- a/modules/packages/nuget/symbol_extractor.go +++ b/modules/packages/nuget/symbol_extractor.go @@ -42,7 +42,7 @@ func (l PortablePdbList) Close() { func ExtractPortablePdb(r io.ReaderAt, size int64) (PortablePdbList, error) { archive, err := zip.NewReader(r, size) if err != nil { - return nil, util.NewInvalidArgumentErrorf("not a valid symbol package: %v", err) + return nil, util.NewInvalidArgumentErrorf("not a valid symbols package: %v", err) } var pdbs PortablePdbList From e66525edb3e66382dfe5df60be4a23469c290994 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 1 Apr 2026 23:15:04 +0200 Subject: [PATCH 3/6] Address review: use NuGet API key auth and assert package persistence Co-Authored-By: Claude (Opus 4.6) --- tests/integration/api_packages_nuget_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/integration/api_packages_nuget_test.go b/tests/integration/api_packages_nuget_test.go index 2314ab45d6163..cf93c22cd02d2 100644 --- a/tests/integration/api_packages_nuget_test.go +++ b/tests/integration/api_packages_nuget_test.go @@ -937,7 +937,7 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) packageContent := createPackage(packageName, packageVersion).Bytes() - // Simulate dotnet nuget push which sends multipart/form-data + // Simulate dotnet nuget push which sends multipart/form-data with X-NuGet-ApiKey auth var body bytes.Buffer mpw := multipart.NewWriter(&body) part, err := mpw.CreateFormFile("package", "package.nupkg") @@ -948,10 +948,14 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) assert.NoError(t, err) req := NewRequestWithBody(t, "PUT", url, &body). - AddBasicAuth(user.Name). SetHeader("Content-Type", mpw.FormDataContentType()) + addNuGetAPIKeyHeader(req, writeToken) MakeRequest(t, req, http.StatusCreated) + pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeNuGet) + assert.NoError(t, err) + assert.Len(t, pvs, 1) + // Clean up req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, packageName, packageVersion)). AddBasicAuth(user.Name) From f04358195e72400897d0a04849356b077fe087c8 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 1 Apr 2026 23:36:56 +0200 Subject: [PATCH 4/6] =?UTF-8?q?Revert=20UploadStream=20changes=20=E2=80=94?= =?UTF-8?q?=20unnecessary=20with=20Go=201.26?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ParseMultipartForm always returns an error when it cannot parse the form, so MultipartForm is never nil after a nil-error return. The nil guard and application/x-www-form-urlencoded removal were defensive but unreachable. Co-Authored-By: Claude (Opus 4.6) --- services/context/context_request.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/services/context/context_request.go b/services/context/context_request.go index 86a20c5e67395..984b9ac793e8d 100644 --- a/services/context/context_request.go +++ b/services/context/context_request.go @@ -13,16 +13,17 @@ import ( // Only form files need to get closed. func (ctx *Context) UploadStream() (rd io.ReadCloser, needToClose bool, err error) { contentType := strings.ToLower(ctx.Req.Header.Get("Content-Type")) - if strings.HasPrefix(contentType, "multipart/form-data") { + if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") || strings.HasPrefix(contentType, "multipart/form-data") { if err := ctx.Req.ParseMultipartForm(32 << 20); err != nil { return nil, false, err } - if ctx.Req.MultipartForm != nil { - for _, files := range ctx.Req.MultipartForm.File { - if len(files) > 0 { - r, err := files[0].Open() - return r, true, err - } + if ctx.Req.MultipartForm.File == nil { + return nil, false, http.ErrMissingFile + } + for _, files := range ctx.Req.MultipartForm.File { + if len(files) > 0 { + r, err := files[0].Open() + return r, true, err } } return nil, false, http.ErrMissingFile From b894b5383240bcd4e0261359499c6bba8a562197 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 1 Apr 2026 23:40:51 +0200 Subject: [PATCH 5/6] Simplify error messages Co-Authored-By: Claude (Opus 4.6) --- modules/packages/nuget/metadata.go | 2 +- modules/packages/nuget/symbol_extractor.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go index c3b708f403ef2..6b44a882cb0e1 100644 --- a/modules/packages/nuget/metadata.go +++ b/modules/packages/nuget/metadata.go @@ -140,7 +140,7 @@ type nuspecPackage struct { func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) { archive, err := zip.NewReader(r, size) if err != nil { - return nil, util.NewInvalidArgumentErrorf("not a valid NuGet package: %v", err) + return nil, util.NewInvalidArgumentErrorf("invalid package file: %v", err) } for _, file := range archive.File { diff --git a/modules/packages/nuget/symbol_extractor.go b/modules/packages/nuget/symbol_extractor.go index 21d45a0b4e8e8..7a1476b14ad42 100644 --- a/modules/packages/nuget/symbol_extractor.go +++ b/modules/packages/nuget/symbol_extractor.go @@ -42,7 +42,7 @@ func (l PortablePdbList) Close() { func ExtractPortablePdb(r io.ReaderAt, size int64) (PortablePdbList, error) { archive, err := zip.NewReader(r, size) if err != nil { - return nil, util.NewInvalidArgumentErrorf("not a valid symbols package: %v", err) + return nil, util.NewInvalidArgumentErrorf("invalid package file: %v", err) } var pdbs PortablePdbList From c911f312c112807d2d04d8fb1ac8964511de469e Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 2 Apr 2026 06:33:05 +0800 Subject: [PATCH 6/6] Apply suggestions from code review Signed-off-by: wxiaoguang --- modules/packages/nuget/metadata.go | 2 +- modules/packages/nuget/symbol_extractor.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go index 6b44a882cb0e1..ae15e4ec831a1 100644 --- a/modules/packages/nuget/metadata.go +++ b/modules/packages/nuget/metadata.go @@ -140,7 +140,7 @@ type nuspecPackage struct { func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) { archive, err := zip.NewReader(r, size) if err != nil { - return nil, util.NewInvalidArgumentErrorf("invalid package file: %v", err) + return nil, util.NewInvalidArgumentErrorf("unable to parse package meta: %v", err) } for _, file := range archive.File { diff --git a/modules/packages/nuget/symbol_extractor.go b/modules/packages/nuget/symbol_extractor.go index 7a1476b14ad42..5e398151e8c17 100644 --- a/modules/packages/nuget/symbol_extractor.go +++ b/modules/packages/nuget/symbol_extractor.go @@ -42,7 +42,7 @@ func (l PortablePdbList) Close() { func ExtractPortablePdb(r io.ReaderAt, size int64) (PortablePdbList, error) { archive, err := zip.NewReader(r, size) if err != nil { - return nil, util.NewInvalidArgumentErrorf("invalid package file: %v", err) + return nil, util.NewInvalidArgumentErrorf("unable to extract portable pdb: %v", err) } var pdbs PortablePdbList