feat(fuzz): enhance MultiPartForm with metadata APIs (#6486)

* feat(fuzz): enhance `MultiPartForm` with metadata APIs

* add `SetFileMetadata`/`GetFileMetadata` APIs for
  file metadata management.
* implement RFC-2046 boundary validation
  (max 70 chars).
* add boundary validation in `Decode` method.

* fix `filesMetadata` initialization.
* fix mem leak by removing defer from file reading
  loop.
* fix file metadata overwriting by storing first
  file's metadata instead of last.

Closes #6405, #6406

Signed-off-by: Dwi Siswanto <git@dw1.io>

* chore(fuzz): satisfy lint errs

Signed-off-by: Dwi Siswanto <git@dw1.io>

---------

Signed-off-by: Dwi Siswanto <git@dw1.io>
This commit is contained in:
Dwi Siswanto 2025-09-22 23:39:24 +07:00 committed by GitHub
parent 39e9286371
commit d2cf69aebb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 182 additions and 10 deletions

View File

@ -27,7 +27,29 @@ var (
// NewMultiPartForm returns a new MultiPartForm encoder
func NewMultiPartForm() *MultiPartForm {
return &MultiPartForm{}
return &MultiPartForm{
filesMetadata: make(map[string]FileMetadata),
}
}
// SetFileMetadata sets the file metadata for a given field name
func (m *MultiPartForm) SetFileMetadata(fieldName string, metadata FileMetadata) {
if m.filesMetadata == nil {
m.filesMetadata = make(map[string]FileMetadata)
}
m.filesMetadata[fieldName] = metadata
}
// GetFileMetadata gets the file metadata for a given field name
func (m *MultiPartForm) GetFileMetadata(fieldName string) (FileMetadata, bool) {
if m.filesMetadata == nil {
return FileMetadata{}, false
}
metadata, exists := m.filesMetadata[fieldName]
return metadata, exists
}
// IsType returns true if the data is MultiPartForm encoded
@ -125,16 +147,24 @@ func (m *MultiPartForm) ParseBoundary(contentType string) error {
if m.boundary == "" {
return fmt.Errorf("no boundary found in the content type")
}
// NOTE(dwisiswant0): boundary cannot exceed 70 characters according to
// RFC-2046.
if len(m.boundary) > 70 {
return fmt.Errorf("boundary exceeds maximum length of 70 characters")
}
return nil
}
// Decode decodes the data from MultiPartForm format
func (m *MultiPartForm) Decode(data string) (KV, error) {
if m.boundary == "" {
return KV{}, fmt.Errorf("boundary not set, call ParseBoundary first")
}
// Create a buffer from the string data
b := bytes.NewBufferString(data)
// The boundary parameter should be extracted from the Content-Type header of the HTTP request
// which is not available in this context, so this is a placeholder for demonstration.
// You will need to pass the actual boundary value to this function.
r := multipart.NewReader(b, m.boundary)
form, err := r.ReadForm(32 << 20) // 32MB is the max memory used to parse the form
@ -153,30 +183,44 @@ func (m *MultiPartForm) Decode(data string) (KV, error) {
result.Set(key, values[0])
}
}
m.filesMetadata = make(map[string]FileMetadata)
if m.filesMetadata == nil {
m.filesMetadata = make(map[string]FileMetadata)
}
for key, files := range form.File {
fileContents := []interface{}{}
var fileMetadataList []FileMetadata
for _, fileHeader := range files {
file, err := fileHeader.Open()
if err != nil {
return KV{}, err
}
defer func() {
_ = file.Close()
}()
buffer := new(bytes.Buffer)
if _, err := buffer.ReadFrom(file); err != nil {
_ = file.Close()
return KV{}, err
}
_ = file.Close()
fileContents = append(fileContents, buffer.String())
m.filesMetadata[key] = FileMetadata{
fileMetadataList = append(fileMetadataList, FileMetadata{
ContentType: fileHeader.Header.Get("Content-Type"),
Filename: fileHeader.Filename,
}
})
}
result.Set(key, fileContents)
// NOTE(dwisiswant0): store the first file's metadata instead of the
// last one
if len(fileMetadataList) > 0 {
m.filesMetadata[key] = fileMetadataList[0]
}
}
return KVOrderedMap(&result), nil
}

View File

@ -240,3 +240,131 @@ Python
assert.Contains(t, documentsArray, "fake_pdf_content_1")
assert.Contains(t, documentsArray, "fake_pdf_content_2")
}
func TestMultiPartForm_SetGetFileMetadata(t *testing.T) {
form := NewMultiPartForm()
metadata := FileMetadata{
ContentType: "image/jpeg",
Filename: "test.jpg",
}
form.SetFileMetadata("avatar", metadata)
// Test GetFileMetadata for existing field
retrievedMetadata, exists := form.GetFileMetadata("avatar")
assert.True(t, exists)
assert.Equal(t, metadata.ContentType, retrievedMetadata.ContentType)
assert.Equal(t, metadata.Filename, retrievedMetadata.Filename)
// Test GetFileMetadata for non-existing field
_, exists = form.GetFileMetadata("nonexistent")
assert.False(t, exists)
}
func TestMultiPartForm_FilesMetadataInitialization(t *testing.T) {
form := NewMultiPartForm()
assert.NotNil(t, form.filesMetadata)
metadata := FileMetadata{
ContentType: "text/plain",
Filename: "test.txt",
}
form.SetFileMetadata("file", metadata)
retrievedMetadata, exists := form.GetFileMetadata("file")
assert.True(t, exists)
assert.Equal(t, metadata, retrievedMetadata)
}
func TestMultiPartForm_BoundaryValidation(t *testing.T) {
form := NewMultiPartForm()
// Test valid boundary
err := form.ParseBoundary("multipart/form-data; boundary=testboundary")
assert.NoError(t, err)
assert.Equal(t, "testboundary", form.boundary)
// Test missing boundary
err = form.ParseBoundary("multipart/form-data")
assert.Error(t, err)
assert.Contains(t, err.Error(), "no boundary found")
// Test boundary too long (over 70 characters)
longBoundary := "multipart/form-data; boundary=" + string(make([]byte, 71))
for i := range longBoundary[len("multipart/form-data; boundary="):] {
longBoundary = longBoundary[:len("multipart/form-data; boundary=")+i] + "a" + longBoundary[len("multipart/form-data; boundary=")+i+1:]
}
err = form.ParseBoundary(longBoundary)
assert.Error(t, err)
assert.Contains(t, err.Error(), "boundary exceeds maximum length")
}
func TestMultiPartForm_DecodeRequiresBoundary(t *testing.T) {
form := NewMultiPartForm()
// Decode should fail if boundary is not set
_, err := form.Decode("some data")
assert.Error(t, err)
assert.Contains(t, err.Error(), "boundary not set")
}
func TestMultiPartForm_MultipleFilesMetadata(t *testing.T) {
form := NewMultiPartForm()
form.boundary = "----WebKitFormBoundaryMultiFileTest"
// Test with multiple files having the same field name
multipartData := `------WebKitFormBoundaryMultiFileTest
Content-Disposition: form-data; name="documents"; filename="file1.txt"
Content-Type: text/plain
content1
------WebKitFormBoundaryMultiFileTest
Content-Disposition: form-data; name="documents"; filename="file2.txt"
Content-Type: text/plain
content2
------WebKitFormBoundaryMultiFileTest--
`
decoded, err := form.Decode(multipartData)
require.NoError(t, err)
// Verify files are decoded correctly
documents := decoded.Get("documents")
require.NotNil(t, documents)
documentsArray, ok := documents.([]interface{})
require.True(t, ok)
require.Len(t, documentsArray, 2)
assert.Contains(t, documentsArray, "content1")
assert.Contains(t, documentsArray, "content2")
// Verify metadata for the field exists (should be from the first file)
metadata, exists := form.GetFileMetadata("documents")
assert.True(t, exists)
assert.Equal(t, "text/plain", metadata.ContentType)
assert.Equal(t, "file1.txt", metadata.Filename) // Should be from first file, not last
}
func TestMultiPartForm_SetFileMetadataWithNilMap(t *testing.T) {
form := &MultiPartForm{}
// SetFileMetadata should handle nil filesMetadata
metadata := FileMetadata{
ContentType: "application/pdf",
Filename: "document.pdf",
}
form.SetFileMetadata("doc", metadata)
// Should be able to retrieve the metadata
retrievedMetadata, exists := form.GetFileMetadata("doc")
assert.True(t, exists)
assert.Equal(t, metadata, retrievedMetadata)
}
func TestMultiPartForm_GetFileMetadataWithNilMap(t *testing.T) {
form := &MultiPartForm{}
// GetFileMetadata should handle nil filesMetadata gracefully
_, exists := form.GetFileMetadata("anything")
assert.False(t, exists)
}