mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-18 05:15:24 +00:00
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:
parent
39e9286371
commit
d2cf69aebb
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user