From ad87ec7f04dd39e2fbd1585372207f7cdcffbcca Mon Sep 17 00:00:00 2001 From: james-yusuke Date: Wed, 1 Jul 2026 21:33:21 +0900 Subject: [PATCH] fix(fuzz): preserve form parameters with shared prefixes --- pkg/fuzz/component/query_test.go | 21 +++++++++++++++++++ pkg/fuzz/dataformat/form.go | 11 ++++++++-- pkg/fuzz/dataformat/form_test.go | 35 ++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 pkg/fuzz/dataformat/form_test.go diff --git a/pkg/fuzz/component/query_test.go b/pkg/fuzz/component/query_test.go index 00d93c69d1..5de172cb47 100644 --- a/pkg/fuzz/component/query_test.go +++ b/pkg/fuzz/component/query_test.go @@ -44,3 +44,24 @@ func TestQueryComponent(t *testing.T) { require.Equal(t, "foo=baz", rebuilt.RawQuery, "unexpected query string") require.Equal(t, "https://example.com?foo=baz", rebuilt.String(), "unexpected url") } + +func TestQueryComponentDoesNotMergePrefixParameterNames(t *testing.T) { + req, err := retryablehttp.NewRequest(http.MethodGet, "https://example.com?foo=a&foobar=b&foobar=c", nil) + if err != nil { + t.Fatal(err) + } + + query := NewQuery() + _, err = query.Parse(req) + if err != nil { + t.Fatal(err) + } + + rebuilt, err := query.Rebuild() + if err != nil { + t.Fatal(err) + } + + require.Equal(t, "foo=a&foobar=b&foobar=c", rebuilt.RawQuery, "unexpected query string") + require.Equal(t, "https://example.com?foo=a&foobar=b&foobar=c", rebuilt.String(), "unexpected url") +} diff --git a/pkg/fuzz/dataformat/form.go b/pkg/fuzz/dataformat/form.go index a8e59c0b4d..fcbd03393a 100644 --- a/pkg/fuzz/dataformat/form.go +++ b/pkg/fuzz/dataformat/form.go @@ -4,7 +4,6 @@ import ( "fmt" "regexp" "strconv" - "strings" "github.com/projectdiscovery/gologger" mapsutil "github.com/projectdiscovery/utils/maps" @@ -60,7 +59,7 @@ func (f *Form) Encode(data KV) (string, error) { // here origKey is base key without _1, _2 etc. if origKey != "" && !reNormalized.MatchString(origKey) { params.Iterate(func(key string, value []string) bool { - if strings.HasPrefix(key, origKey) && reNormalized.MatchString(key) { + if baseKey, ok := normalizedKeyBase(key); ok && baseKey == origKey { m := map[string]string{} if normalized[origKey] != nil { m = normalized[origKey] @@ -128,6 +127,14 @@ func (f *Form) Encode(data KV) (string, error) { return encoded, nil } +func normalizedKeyBase(key string) (string, bool) { + match := reNormalized.FindStringIndex(key) + if match == nil { + return "", false + } + return key[:match[0]], true +} + // Decode decodes the data from Form format func (f *Form) Decode(data string) (KV, error) { ordered_params := urlutil.NewOrderedParams() diff --git a/pkg/fuzz/dataformat/form_test.go b/pkg/fuzz/dataformat/form_test.go new file mode 100644 index 0000000000..7a5a7dbb31 --- /dev/null +++ b/pkg/fuzz/dataformat/form_test.go @@ -0,0 +1,35 @@ +package dataformat + +import "testing" + +func TestFormDecodeEncode_DuplicateParameters(t *testing.T) { + form := NewForm() + decoded, err := form.Decode("foo=a&foo=b&foo=c") + if err != nil { + t.Fatal(err) + } + + encoded, err := form.Encode(decoded) + if err != nil { + t.Fatal(err) + } + if encoded != "foo=a&foo=b&foo=c" { + t.Fatalf("unexpected form encoding: %q", encoded) + } +} + +func TestFormDecodeEncode_DoesNotMergePrefixParameterNames(t *testing.T) { + form := NewForm() + decoded, err := form.Decode("foo=a&foobar=b&foobar=c") + if err != nil { + t.Fatal(err) + } + + encoded, err := form.Encode(decoded) + if err != nil { + t.Fatal(err) + } + if encoded != "foo=a&foobar=b&foobar=c" { + t.Fatalf("unexpected form encoding: %q", encoded) + } +}