Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines 622 to +623

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

part.Read(multipartBody) reads only once into a fixed 1400-byte buffer. If a multipart part exceeds 1400 bytes, Read can return n==len(buf) with err==nil, and the code will proceed with truncated data (e.g., incomplete JSON or binary). Consider reading the full part (e.g., io.ReadAll / io.ReadAll(io.LimitReader(...)) or a loop until io.EOF) so valid requests aren’t corrupted or rejected.

Copilot uses AI. Check for mistakes.
// 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]

Expand Down
29 changes: 29 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
}
}
Loading