nuclei/pkg/fuzz/dataformat/form.go

158 lines
4.5 KiB
Go

package dataformat
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/projectdiscovery/gologger"
mapsutil "github.com/projectdiscovery/utils/maps"
urlutil "github.com/projectdiscovery/utils/url"
)
const (
normalizedRegex = `_(\d+)$`
)
var (
reNormalized = regexp.MustCompile(normalizedRegex)
)
// == Handling Duplicate Query Parameters / Form Data ==
// Nuclei supports fuzzing duplicate query parameters by internally normalizing
// them and denormalizing them back when creating request this normalization
// can be leveraged to specify custom fuzzing behaviour in template as well
// if a query like `?foo=bar&foo=baz&foo=fuzzz` is provided, it will be normalized to
// foo_1=bar , foo_2=baz , foo=fuzzz (i.e last value is given original key which is usual behaviour in HTTP and its implementations)
// this way this change does not break any existing rules in template given by keys-regex or keys
// At same time if user wants to specify 2nd or 1st duplicate value in template, they can use foo_1 or foo_2 in keys-regex or keys
// Note: By default all duplicate query parameters are fuzzed
type Form struct{}
var (
_ DataFormat = &Form{}
)
// NewForm returns a new Form encoder
func NewForm() *Form {
return &Form{}
}
// IsType returns true if the data is Form encoded
func (f *Form) IsType(data string) bool {
return false
}
// Encode encodes the data into Form format
func (f *Form) Encode(data KV) (string, error) {
params := urlutil.NewOrderedParams()
data.Iterate(func(key string, value any) bool {
params.Add(key, fmt.Sprint(value))
return true
})
normalized := map[string]map[string]string{}
// Normalize the data
for _, origKey := range data.OrderedMap.GetKeys() {
// 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) {
m := map[string]string{}
if normalized[origKey] != nil {
m = normalized[origKey]
}
if len(value) == 1 {
m[key] = value[0]
} else {
m[key] = ""
}
normalized[origKey] = m
params.Del(key)
}
return true
})
}
}
if len(normalized) > 0 {
for k, v := range normalized {
maxIndex := -1
for key := range v {
matches := reNormalized.FindStringSubmatch(key)
if len(matches) == 2 {
dataIdx, err := strconv.Atoi(matches[1])
if err != nil {
gologger.Verbose().Msgf("error converting normalized index(%v) to integer: %v", matches[1], err)
continue
}
if dataIdx > maxIndex {
maxIndex = dataIdx
}
}
}
if maxIndex >= 0 { // Ensure the slice is only created if maxIndex is valid
data := make([]string, maxIndex+1) // Ensure the slice is large enough
for key, value := range v {
matches := reNormalized.FindStringSubmatch(key)
if len(matches) == 2 {
dataIdx, err := strconv.Atoi(matches[1]) // Error already checked above
if err != nil {
gologger.Verbose().Msgf("error converting data index to integer: %v", err)
continue
}
// Validate dataIdx to avoid index out of range errors
if dataIdx > 0 && dataIdx <= len(data) {
data[dataIdx-1] = value // Use dataIdx-1 since slice is 0-indexed
} else {
gologger.Verbose().Msgf("data index out of range: %d", dataIdx)
}
}
}
if len(params.Get(k)) > 0 {
data[maxIndex] = fmt.Sprint(params.Get(k)) // Use maxIndex which is the last index
}
// remove existing
params.Del(k)
if len(data) > 0 {
params.Add(k, data...)
}
}
}
}
encoded := params.Encode()
return encoded, nil
}
// Decode decodes the data from Form format
func (f *Form) Decode(data string) (KV, error) {
ordered_params := urlutil.NewOrderedParams()
ordered_params.Merge(data)
values := mapsutil.NewOrderedMap[string, any]()
ordered_params.Iterate(func(key string, value []string) bool {
if len(value) == 1 {
values.Set(key, value[0])
} else {
// in case of multiple query params in form data
// last value is considered and previous values are exposed with _1, _2, _3 etc.
// note that last value will not be included in _1, _2, _3 etc.
for i := 0; i < len(value)-1; i++ {
values.Set(key+"_"+strconv.Itoa(i+1), value[i])
}
values.Set(key, value[len(value)-1])
}
return true
})
return KVOrderedMap(&values), nil
}
// Name returns the name of the encoder
func (f *Form) Name() string {
return FormDataFormat
}