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..c48ebf59 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 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{} + + 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) + }) + }) + } +}