mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-18 05:15:24 +00:00
* 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>
371 lines
12 KiB
Go
371 lines
12 KiB
Go
package dataformat
|
|
|
|
import (
|
|
"testing"
|
|
|
|
mapsutil "github.com/projectdiscovery/utils/maps"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestMultiPartFormEncode(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
fields map[string]any
|
|
wantErr bool
|
|
expected map[string]any
|
|
}{
|
|
{
|
|
name: "duplicate fields ([]string) - checkbox scenario",
|
|
fields: map[string]any{
|
|
"interests": []string{"sports", "music", "reading"},
|
|
"colors": []string{"red", "blue"},
|
|
},
|
|
expected: map[string]any{
|
|
"interests": []string{"sports", "music", "reading"},
|
|
"colors": []string{"red", "blue"},
|
|
},
|
|
},
|
|
{
|
|
name: "single string fields - backward compatibility",
|
|
fields: map[string]any{
|
|
"username": "john",
|
|
"email": "john@example.com",
|
|
},
|
|
expected: map[string]any{
|
|
"username": "john",
|
|
"email": "john@example.com",
|
|
},
|
|
},
|
|
{
|
|
name: "mixed types",
|
|
fields: map[string]any{
|
|
"string": "text",
|
|
"array": []string{"item1", "item2"},
|
|
"number": 42, // tests fmt.Sprint fallback
|
|
"float": 3.14, // tests float conversion
|
|
"boolean": true, // tests boolean conversion
|
|
"zero": 0, // tests zero value
|
|
"emptyStr": "", // tests empty string
|
|
"negative": -123, // tests negative number
|
|
"nil": nil, // tests nil value
|
|
"mixedArray": []any{"str", 123, false, nil}, // tests mixed type array
|
|
},
|
|
expected: map[string]any{
|
|
"string": "text",
|
|
"array": []string{"item1", "item2"},
|
|
"number": "42", // numbers are converted to strings in multipart
|
|
"float": "3.14", // floats are converted to strings
|
|
"boolean": "true", // booleans are converted to strings
|
|
"zero": "0", // zero value converted to string
|
|
"emptyStr": "", // empty string remains empty
|
|
"negative": "-123", // negative numbers converted to strings
|
|
"nil": "", // nil values converted to "" string
|
|
"mixedArray": []string{"str", "123", "false", ""}, // mixed array converted to string array
|
|
},
|
|
},
|
|
{
|
|
name: "empty array - should not appear in output",
|
|
fields: map[string]any{
|
|
"emptyArray": []string{},
|
|
"normalField": "value",
|
|
},
|
|
expected: map[string]any{
|
|
"normalField": "value",
|
|
// emptyArray should not appear in decoded output
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("Test panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
form := NewMultiPartForm()
|
|
form.boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"
|
|
|
|
kv := mapsutil.NewOrderedMap[string, any]()
|
|
for k, v := range tt.fields {
|
|
kv.Set(k, v)
|
|
}
|
|
|
|
encoded, err := form.Encode(KVOrderedMap(&kv))
|
|
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
|
|
// Decode the encoded multipart data
|
|
decoded, err := form.Decode(encoded)
|
|
require.NoError(t, err)
|
|
|
|
// Compare decoded values with expected values
|
|
for expectedKey, expectedValue := range tt.expected {
|
|
actualValue := decoded.Get(expectedKey)
|
|
switch expected := expectedValue.(type) {
|
|
case []string:
|
|
actual, ok := actualValue.([]string)
|
|
require.True(t, ok, "Expected []string for key %s, got %T", expectedKey, actualValue)
|
|
assert.ElementsMatch(t, expected, actual, "Values mismatch for key %s", expectedKey)
|
|
case []any:
|
|
actual, ok := actualValue.([]any)
|
|
require.True(t, ok, "Expected []any for key %s, got %T", expectedKey, actualValue)
|
|
assert.ElementsMatch(t, expected, actual, "Values mismatch for key %s", expectedKey)
|
|
case string:
|
|
actual, ok := actualValue.(string)
|
|
require.True(t, ok, "Expected string for key %s, got %T", expectedKey, actualValue)
|
|
assert.Equal(t, expected, actual, "Values mismatch for key %s", expectedKey)
|
|
default:
|
|
assert.Equal(t, expected, actualValue, "Values mismatch for key %s", expectedKey)
|
|
}
|
|
}
|
|
|
|
// Ensure no unexpected keys are present in decoded output
|
|
decoded.Iterate(func(key string, value any) bool {
|
|
_, exists := tt.expected[key]
|
|
assert.True(t, exists, "Unexpected key %s found in decoded output", key)
|
|
return true
|
|
})
|
|
|
|
t.Logf("Encoded output:\n%s", encoded)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMultiPartFormRoundTrip(t *testing.T) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("Test panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
form := NewMultiPartForm()
|
|
form.boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"
|
|
|
|
original := mapsutil.NewOrderedMap[string, any]()
|
|
original.Set("username", "john")
|
|
original.Set("interests", []string{"sports", "music", "reading"})
|
|
|
|
encoded, err := form.Encode(KVOrderedMap(&original))
|
|
require.NoError(t, err)
|
|
|
|
decoded, err := form.Decode(encoded)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "john", decoded.Get("username"))
|
|
assert.ElementsMatch(t, []string{"sports", "music", "reading"}, decoded.Get("interests"))
|
|
|
|
t.Logf("Encoded output:\n%s", encoded)
|
|
}
|
|
|
|
func TestMultiPartFormFileUpload(t *testing.T) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("Test panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
// Test decoding of a manually crafted multipart form with files
|
|
form := NewMultiPartForm()
|
|
form.boundary = "----WebKitFormBoundaryFileUploadTest"
|
|
|
|
// Manually craft a multipart form with file uploads
|
|
multipartData := `------WebKitFormBoundaryFileUploadTest
|
|
Content-Disposition: form-data; name="name"
|
|
|
|
John Doe
|
|
------WebKitFormBoundaryFileUploadTest
|
|
Content-Disposition: form-data; name="email"
|
|
|
|
john@example.com
|
|
------WebKitFormBoundaryFileUploadTest
|
|
Content-Disposition: form-data; name="profile_picture"; filename="profile.jpg"
|
|
Content-Type: image/jpeg
|
|
|
|
fake_jpeg_binary_data_here
|
|
------WebKitFormBoundaryFileUploadTest
|
|
Content-Disposition: form-data; name="documents"; filename="resume.pdf"
|
|
Content-Type: application/pdf
|
|
|
|
fake_pdf_content_1
|
|
------WebKitFormBoundaryFileUploadTest
|
|
Content-Disposition: form-data; name="documents"; filename="cover_letter.pdf"
|
|
Content-Type: application/pdf
|
|
|
|
fake_pdf_content_2
|
|
------WebKitFormBoundaryFileUploadTest
|
|
Content-Disposition: form-data; name="skills"
|
|
|
|
Go
|
|
------WebKitFormBoundaryFileUploadTest
|
|
Content-Disposition: form-data; name="skills"
|
|
|
|
JavaScript
|
|
------WebKitFormBoundaryFileUploadTest
|
|
Content-Disposition: form-data; name="skills"
|
|
|
|
Python
|
|
------WebKitFormBoundaryFileUploadTest--
|
|
`
|
|
|
|
// Test decoding
|
|
decoded, err := form.Decode(multipartData)
|
|
require.NoError(t, err)
|
|
|
|
// Verify regular fields
|
|
assert.Equal(t, "John Doe", decoded.Get("name"))
|
|
assert.Equal(t, "john@example.com", decoded.Get("email"))
|
|
assert.Equal(t, []string{"Go", "JavaScript", "Python"}, decoded.Get("skills"))
|
|
|
|
// Verify file fields
|
|
profilePicture := decoded.Get("profile_picture")
|
|
require.NotNil(t, profilePicture)
|
|
profileArray, ok := profilePicture.([]interface{})
|
|
require.True(t, ok, "Expected []interface{} for profile_picture")
|
|
require.Len(t, profileArray, 1)
|
|
assert.Equal(t, "fake_jpeg_binary_data_here", profileArray[0])
|
|
|
|
documents := decoded.Get("documents")
|
|
require.NotNil(t, documents)
|
|
documentsArray, ok := documents.([]interface{})
|
|
require.True(t, ok, "Expected []interface{} for documents")
|
|
require.Len(t, documentsArray, 2)
|
|
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)
|
|
}
|