From 615098af9ed73cd34fd0a1ee2d507f29c8b12858 Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Tue, 21 Apr 2026 09:47:13 -0700 Subject: [PATCH 1/2] MultipartDeserialize: guard against nil part on malformed body mime/multipart.Reader.NextPart can return a nil *Part alongside a non-EOF error when the body is not valid MIME multipart data (plain text, JSON, truncated multipart, mismatched boundary, empty body, ...). The previous loop treated any non-EOF error as "just break" and then dereferenced part.Header, so every NF that calls MultipartDeserialize (SMF on POST /nsmf-pdusession/v1/sm-contexts, UDM, AUSF) panicked on a crafted request and returned HTTP 500. Treat non-EOF errors from NextPart and non-EOF errors from part.Read as real failures and return them wrapped. Also fix the inverted check on part.Read: the code was returning from the loop when err == nil, which dropped every successfully read part; swap to the correct `err != nil` branch (ignoring io.EOF, which just marks end-of-part). Add a regression test that feeds three malformed bodies (plain text, empty, JSON) through MultipartDeserialize and asserts no panic plus an error. Pre-fix the plain-text case panicked on part.Header.Get. Fixes free5gc/free5gc#1026. Signed-off-by: SAY-5 --- client.go | 28 +++++++++++++++++++++------- client_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/client.go b/client.go index cba9969c..41c7f397 100644 --- a/client.go +++ b/client.go @@ -598,21 +598,35 @@ func MultipartDeserialize(b []byte, v interface{}, boundary string) (err error) contentIDIndex := make(map[string]int) for { - var part, nextPart *multipart.Part + var part *multipart.Part multipartBody := make([]byte, 1400) - // if no remian part, break this loop - if nextPart, err = r.NextPart(); err == io.EOF { + // if no remaining part, break this loop + part, err = r.NextPart() + if err == io.EOF { break - } else { - part = nextPart + } + // A non-EOF error means the multipart body is malformed (truncated + // part, mismatched boundary, or a body that is not multipart at all). + // NextPart can return a nil part in that case; propagate the error so + // the caller can respond with 400 instead of panicking on part.Header. + if err != nil { + return fmt.Errorf("MultipartDeserialize: read next part: %w", err) + } + if part == nil { + return fmt.Errorf("MultipartDeserialize: nil part with nil error") } contentType := part.Header.Get("Content-Type") var n int n, err = part.Read(multipartBody) - if err == nil { - return err + // Read returns err == io.EOF when the part is fully consumed; any + // other error (e.g. unexpected EOF on a truncated part) is a real + // failure. Previously this branch was inverted and returned the nil + // error from a successful read, leaving n as the valid byte count + // but silently aborting the loop. + if err != nil && err != io.EOF { + return fmt.Errorf("MultipartDeserialize: read part body: %w", err) } multipartBody = multipartBody[:n] diff --git a/client_test.go b/client_test.go index eb7070f8..af0ad0b8 100644 --- a/client_test.go +++ b/client_test.go @@ -60,3 +60,32 @@ func TestParameterToString(t *testing.T) { }) } } + +// TestMultipartDeserialize_MalformedBody pins the fix for free5gc/free5gc#1026. +// Before the fix, a body whose Content-Type declared multipart/related but +// whose payload was not a valid MIME multipart stream would panic on +// part.Header.Get at client.go:608 because mime/multipart.NextPart can return +// a nil *Part alongside a non-EOF error. The SMF then surfaced HTTP 500 on +// every malformed POST /nsmf-pdusession/v1/sm-contexts. +func TestMultipartDeserialize_MalformedBody(t *testing.T) { + type dummy struct{} + + cases := []struct { + name string + body []byte + }{ + {name: "plain text body", body: []byte("hello, not multipart")}, + {name: "empty body", body: []byte("")}, + {name: "json body", body: []byte(`{"foo":"bar"}`)}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var out dummy + require.NotPanics(t, func() { + err := MultipartDeserialize(tc.body, &out, "boundaryX") + require.Error(t, err) + }) + }) + } +} From 61823a35ac7c56e97a7e806d94b136bd2554747f Mon Sep 17 00:00:00 2001 From: Meng Han Hsieh Date: Sat, 25 Apr 2026 22:52:36 +0800 Subject: [PATCH 2/2] Update client_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- client_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client_test.go b/client_test.go index af0ad0b8..c48ebf59 100644 --- a/client_test.go +++ b/client_test.go @@ -64,9 +64,9 @@ func TestParameterToString(t *testing.T) { // TestMultipartDeserialize_MalformedBody pins the fix for free5gc/free5gc#1026. // Before the fix, a body whose Content-Type declared multipart/related but // whose payload was not a valid MIME multipart stream would panic on -// part.Header.Get at client.go:608 because mime/multipart.NextPart can return -// a nil *Part alongside a non-EOF error. The SMF then surfaced HTTP 500 on -// every malformed POST /nsmf-pdusession/v1/sm-contexts. +// part.Header.Get in MultipartDeserialize because mime/multipart.NextPart can +// return a nil *Part alongside a non-EOF error. The SMF then surfaced HTTP +// 500 on every malformed POST /nsmf-pdusession/v1/sm-contexts. func TestMultipartDeserialize_MalformedBody(t *testing.T) { type dummy struct{}