From 3821d30cf1647fe886aa5e2fe06ac992df460a30 Mon Sep 17 00:00:00 2001
From: Matt Heon <mheon@redhat.com>
Date: Wed, 3 Jul 2024 13:31:50 -0400
Subject: [PATCH] Update gorilla/schema to v1.4.1 to fix CVE-2024-37298
Ref: RH Jira RHEL-45916
Signed-off-by: Matt Heon <mheon@redhat.com>
go.mod | 2 +-
go.sum | 4 +-
.../github.com/gorilla/schema/.editorconfig | 20 ++++
vendor/github.com/gorilla/schema/.gitignore | 1 +
vendor/github.com/gorilla/schema/LICENSE | 2 +-
vendor/github.com/gorilla/schema/Makefile | 34 ++++++
vendor/github.com/gorilla/schema/README.md | 39 ++++++-
vendor/github.com/gorilla/schema/cache.go | 28 +++--
vendor/github.com/gorilla/schema/converter.go | 77 +++++++++++++
vendor/github.com/gorilla/schema/decoder.go | 103 +++++++++++++++++-
vendor/github.com/gorilla/schema/encoder.go | 17 ++-
vendor/modules.txt | 4 +-
12 files changed, 307 insertions(+), 24 deletions(-)
create mode 100644 vendor/github.com/gorilla/schema/.editorconfig
create mode 100644 vendor/github.com/gorilla/schema/.gitignore
create mode 100644 vendor/github.com/gorilla/schema/Makefile
@@ -36,7 +36,7 @@ require (
github.com/google/uuid v1.4.0
github.com/gorilla/handlers v1.5.2
github.com/gorilla/mux v1.8.1
- github.com/gorilla/schema v1.2.0
+ github.com/gorilla/schema v1.4.1
github.com/hashicorp/go-multierror v1.1.1
github.com/hugelgupf/p9 v0.3.1-0.20230822151754-54f5c5530921
github.com/json-iterator/go v1.1.12
@@ -622,8 +622,8 @@ github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
-github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
-github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
+github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
+github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
new file mode 100644
@@ -0,0 +1,20 @@
+; https://editorconfig.org/
+
+root = true
+
+[*]
+insert_final_newline = true
+charset = utf-8
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 2
+
+[{Makefile,go.mod,go.sum,*.go,.gitmodules}]
+indent_style = tab
+indent_size = 4
+
+[*.md]
+indent_size = 4
+trim_trailing_whitespace = false
+
+eclint_indent_style = unset
\ No newline at end of file
new file mode 100644
@@ -0,0 +1 @@
+coverage.coverprofile
@@ -1,4 +1,4 @@
-Copyright (c) 2012 Rodrigo Moraes. All rights reserved.
+Copyright (c) 2023 The Gorilla Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
new file mode 100644
@@ -0,0 +1,34 @@
+GO_LINT=$(shell which golangci-lint 2> /dev/null || echo '')
+GO_LINT_URI=github.com/golangci/golangci-lint/cmd/golangci-lint@latest
+
+GO_SEC=$(shell which gosec 2> /dev/null || echo '')
+GO_SEC_URI=github.com/securego/gosec/v2/cmd/gosec@latest
+
+GO_VULNCHECK=$(shell which govulncheck 2> /dev/null || echo '')
+GO_VULNCHECK_URI=golang.org/x/vuln/cmd/govulncheck@latest
+
+.PHONY: golangci-lint
+golangci-lint:
+ $(if $(GO_LINT), ,go install $(GO_LINT_URI))
+ @echo "##### Running golangci-lint"
+ golangci-lint run -v
+
+.PHONY: gosec
+gosec:
+ $(if $(GO_SEC), ,go install $(GO_SEC_URI))
+ @echo "##### Running gosec"
+ gosec ./...
+
+.PHONY: govulncheck
+govulncheck:
+ $(if $(GO_VULNCHECK), ,go install $(GO_VULNCHECK_URI))
+ @echo "##### Running govulncheck"
+ govulncheck ./...
+
+.PHONY: verify
+verify: golangci-lint gosec govulncheck
+
+.PHONY: test
+test:
+ @echo "##### Running tests"
+ go test -race -cover -coverprofile=coverage.coverprofile -covermode=atomic -v ./...
\ No newline at end of file
@@ -1,8 +1,12 @@
-schema
-======
-[](https://godoc.org/github.com/gorilla/schema) [](https://travis-ci.org/gorilla/schema)
-[](https://sourcegraph.com/github.com/gorilla/schema?badge)
+# gorilla/schema
+
+[](https://codecov.io/github/gorilla/schema)
+[](https://godoc.org/github.com/gorilla/schema)
+[](https://sourcegraph.com/github.com/gorilla/schema?badge)
+
+
+
Package gorilla/schema converts structs to and from form values.
@@ -83,7 +87,32 @@ The supported field types in the struct are:
Unsupported types are simply ignored, however custom types can be registered to be converted.
-More examples are available on the Gorilla website: https://www.gorillatoolkit.org/pkg/schema
+## Setting Defaults
+
+It is possible to set default values when encoding/decoding by using the `default` tag option. The value of `default` is applied when a field has a zero value, a pointer has a nil value, or a slice is empty.
+
+```go
+type Person struct {
+ Phone string `schema:"phone,default:+123456"` // custom name
+ Age int `schema:"age,default:21"`
+ Admin bool `schema:"admin,default:false"`
+ Balance float64 `schema:"balance,default:10.0"`
+ Friends []string `schema:friends,default:john|bob`
+}
+```
+
+The `default` tag option is supported for the following types:
+
+* bool
+* float variants (float32, float64)
+* int variants (int, int8, int16, int32, int64)
+* uint variants (uint, uint8, uint16, uint32, uint64)
+* string
+* a slice of the above types. As shown in the example above, `|` should be used to separate between slice items.
+* a pointer to one of the above types (pointer to slice and slice of pointers are not supported).
+
+> [!NOTE]
+> Because primitive types like int, float, bool, unint and their variants have their default (or zero) values set by Golang, it is not possible to distinguish them from a provided value when decoding/encoding form values. In this case, the value provided by the `default` option tag will be always applied. For example, let's assume that the value submitted in the form for `balance` is `0.0` then the default of `10.0` will be applied, even if `0.0` is part of the form data for the `balance` field. In such cases, it is highly recommended to use pointers to allow schema to distinguish between when a form field has no provided value and when a form has a value equal to the corresponding default set by Golang for a particular type. If the type of the `Balance` field above is changed to `*float64`, then the zero value would be `nil`. In this case, if the form data value for `balance` is `0.0`, then the default will not be applied.
## License
@@ -12,7 +12,7 @@ import (
"sync"
)
-var invalidPath = errors.New("schema: invalid path")
+var errInvalidPath = errors.New("schema: invalid path")
// newCache returns a new cache.
func newCache() *cache {
@@ -53,13 +53,13 @@ func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) {
keys := strings.Split(p, ".")
for i := 0; i < len(keys); i++ {
if t.Kind() != reflect.Struct {
- return nil, invalidPath
+ return nil, errInvalidPath
}
if struc = c.get(t); struc == nil {
- return nil, invalidPath
+ return nil, errInvalidPath
}
if field = struc.get(keys[i]); field == nil {
- return nil, invalidPath
+ return nil, errInvalidPath
}
// Valid field. Append index.
path = append(path, field.name)
@@ -72,10 +72,10 @@ func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) {
// So checking i+2 is not necessary anymore.
i++
if i+1 > len(keys) {
- return nil, invalidPath
+ return nil, errInvalidPath
}
if index64, err = strconv.ParseInt(keys[i], 10, 0); err != nil {
- return nil, invalidPath
+ return nil, errInvalidPath
}
parts = append(parts, pathPart{
path: path,
@@ -197,6 +197,7 @@ func (c *cache) createField(field reflect.StructField, parentAlias string) *fiel
isSliceOfStructs: isSlice && isStruct,
isAnonymous: field.Anonymous,
isRequired: options.Contains("required"),
+ defaultValue: options.getDefaultOptionValue(),
}
}
@@ -246,8 +247,9 @@ type fieldInfo struct {
// isSliceOfStructs indicates if the field type is a slice of structs.
isSliceOfStructs bool
// isAnonymous indicates whether the field is embedded in the struct.
- isAnonymous bool
- isRequired bool
+ isAnonymous bool
+ isRequired bool
+ defaultValue string
}
func (f *fieldInfo) paths(prefix string) []string {
@@ -303,3 +305,13 @@ func (o tagOptions) Contains(option string) bool {
}
return false
}
+
+func (o tagOptions) getDefaultOptionValue() string {
+ for _, s := range o {
+ if strings.HasPrefix(s, "default:") {
+ return strings.Split(s, ":")[1]
+ }
+ }
+
+ return ""
+}
@@ -143,3 +143,80 @@ func convertUint64(value string) reflect.Value {
}
return invalidValue
}
+
+func convertPointer(k reflect.Kind, value string) reflect.Value {
+ switch k {
+ case boolType:
+ if v := convertBool(value); v.IsValid() {
+ converted := v.Bool()
+ return reflect.ValueOf(&converted)
+ }
+ case float32Type:
+ if v := convertFloat32(value); v.IsValid() {
+ converted := float32(v.Float())
+ return reflect.ValueOf(&converted)
+ }
+ case float64Type:
+ if v := convertFloat64(value); v.IsValid() {
+ converted := float64(v.Float())
+ return reflect.ValueOf(&converted)
+ }
+ case intType:
+ if v := convertInt(value); v.IsValid() {
+ converted := int(v.Int())
+ return reflect.ValueOf(&converted)
+ }
+ case int8Type:
+ if v := convertInt8(value); v.IsValid() {
+ converted := int8(v.Int())
+ return reflect.ValueOf(&converted)
+ }
+ case int16Type:
+ if v := convertInt16(value); v.IsValid() {
+ converted := int16(v.Int())
+ return reflect.ValueOf(&converted)
+ }
+ case int32Type:
+ if v := convertInt32(value); v.IsValid() {
+ converted := int32(v.Int())
+ return reflect.ValueOf(&converted)
+ }
+ case int64Type:
+ if v := convertInt64(value); v.IsValid() {
+ converted := int64(v.Int())
+ return reflect.ValueOf(&converted)
+ }
+ case stringType:
+ if v := convertString(value); v.IsValid() {
+ converted := v.String()
+ return reflect.ValueOf(&converted)
+ }
+ case uintType:
+ if v := convertUint(value); v.IsValid() {
+ converted := uint(v.Uint())
+ return reflect.ValueOf(&converted)
+ }
+ case uint8Type:
+ if v := convertUint8(value); v.IsValid() {
+ converted := uint8(v.Uint())
+ return reflect.ValueOf(&converted)
+ }
+ case uint16Type:
+ if v := convertUint16(value); v.IsValid() {
+ converted := uint16(v.Uint())
+ return reflect.ValueOf(&converted)
+ }
+ case uint32Type:
+ if v := convertUint32(value); v.IsValid() {
+ converted := uint32(v.Uint())
+ return reflect.ValueOf(&converted)
+ }
+ case uint64Type:
+ if v := convertUint64(value); v.IsValid() {
+ converted := uint64(v.Uint())
+ return reflect.ValueOf(&converted)
+ }
+ }
+
+ return invalidValue
+}
@@ -12,9 +12,13 @@ import (
"strings"
)
+const (
+ defaultMaxSize = 16000
+)
+
// NewDecoder returns a new Decoder.
func NewDecoder() *Decoder {
- return &Decoder{cache: newCache()}
+ return &Decoder{cache: newCache(), maxSize: defaultMaxSize}
}
// Decoder decodes values from a map[string][]string to a struct.
@@ -22,6 +26,7 @@ type Decoder struct {
cache *cache
zeroEmpty bool
ignoreUnknownKeys bool
+ maxSize int
}
// SetAliasTag changes the tag used to locate custom field aliases.
@@ -54,6 +59,13 @@ func (d *Decoder) IgnoreUnknownKeys(i bool) {
d.ignoreUnknownKeys = i
}
+// MaxSize limits the size of slices for URL nested arrays or object arrays.
+// Choose MaxSize carefully; large values may create many zero-value slice elements.
+// Example: "items.100000=apple" would create a slice with 100,000 empty strings.
+func (d *Decoder) MaxSize(size int) {
+ d.maxSize = size
+}
+
// RegisterConverter registers a converter function for a custom type.
func (d *Decoder) RegisterConverter(value interface{}, converterFunc Converter) {
d.cache.registerConverter(value, converterFunc)
@@ -84,6 +96,7 @@ func (d *Decoder) Decode(dst interface{}, src map[string][]string) error {
errors[path] = UnknownKeyError{Key: path}
}
}
+ errors.merge(d.setDefaults(t, v))
errors.merge(d.checkRequired(t, src))
if len(errors) > 0 {
return errors
@@ -91,6 +104,88 @@ func (d *Decoder) Decode(dst interface{}, src map[string][]string) error {
return nil
}
+// setDefaults sets the default values when the `default` tag is specified,
+// default is supported on basic/primitive types and their pointers,
+// nested structs can also have default tags
+func (d *Decoder) setDefaults(t reflect.Type, v reflect.Value) MultiError {
+ struc := d.cache.get(t)
+ if struc == nil {
+ // unexpect, cache.get never return nil
+ return MultiError{"default-" + t.Name(): errors.New("cache fail")}
+ }
+
+ errs := MultiError{}
+
+ if v.Type().Kind() == reflect.Struct {
+ for i := 0; i < v.NumField(); i++ {
+ field := v.Field(i)
+ if field.Type().Kind() == reflect.Ptr && field.IsNil() && v.Type().Field(i).Anonymous {
+ field.Set(reflect.New(field.Type().Elem()))
+ }
+ }
+ }
+
+ for _, f := range struc.fields {
+ vCurrent := v.FieldByName(f.name)
+
+ if vCurrent.Type().Kind() == reflect.Struct && f.defaultValue == "" {
+ errs.merge(d.setDefaults(vCurrent.Type(), vCurrent))
+ } else if isPointerToStruct(vCurrent) && f.defaultValue == "" {
+ errs.merge(d.setDefaults(vCurrent.Elem().Type(), vCurrent.Elem()))
+ }
+
+ if f.defaultValue != "" && f.isRequired {
+ errs.merge(MultiError{"default-" + f.name: errors.New("required fields cannot have a default value")})
+ } else if f.defaultValue != "" && vCurrent.IsZero() && !f.isRequired {
+ if f.typ.Kind() == reflect.Struct {
+ errs.merge(MultiError{"default-" + f.name: errors.New("default option is supported only on: bool, float variants, string, unit variants types or their corresponding pointers or slices")})
+ } else if f.typ.Kind() == reflect.Slice {
+ vals := strings.Split(f.defaultValue, "|")
+
+ // check if slice has one of the supported types for defaults
+ if _, ok := builtinConverters[f.typ.Elem().Kind()]; !ok {
+ errs.merge(MultiError{"default-" + f.name: errors.New("default option is supported only on: bool, float variants, string, unit variants types or their corresponding pointers or slices")})
+ continue
+ }
+
+ defaultSlice := reflect.MakeSlice(f.typ, 0, cap(vals))
+ for _, val := range vals {
+ // this check is to handle if the wrong value is provided
+ convertedVal := builtinConverters[f.typ.Elem().Kind()](val)
+ if !convertedVal.IsValid() {
+ errs.merge(MultiError{"default-" + f.name: fmt.Errorf("failed setting default: %s is not compatible with field %s type", val, f.name)})
+ break
+ }
+ defaultSlice = reflect.Append(defaultSlice, convertedVal)
+ }
+ vCurrent.Set(defaultSlice)
+ } else if f.typ.Kind() == reflect.Ptr {
+ t1 := f.typ.Elem()
+
+ if t1.Kind() == reflect.Struct || t1.Kind() == reflect.Slice {
+ errs.merge(MultiError{"default-" + f.name: errors.New("default option is supported only on: bool, float variants, string, unit variants types or their corresponding pointers or slices")})
+ }
+
+ // this check is to handle if the wrong value is provided
+ if convertedVal := convertPointer(t1.Kind(), f.defaultValue); convertedVal.IsValid() {
+ vCurrent.Set(convertedVal)
+ }
+ } else {
+ // this check is to handle if the wrong value is provided
+ if convertedVal := builtinConverters[f.typ.Kind()](f.defaultValue); convertedVal.IsValid() {
+ vCurrent.Set(builtinConverters[f.typ.Kind()](f.defaultValue))
+ }
+ }
+ }
+ }
+
+ return errs
+}
+
+func isPointerToStruct(v reflect.Value) bool {
+ return !v.IsZero() && v.Type().Kind() == reflect.Ptr && v.Elem().Type().Kind() == reflect.Struct
+}
+
// checkRequired checks whether required fields are empty
//
// check type t recursively if t has struct fields.
@@ -193,7 +288,7 @@ func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values
if v.Type().Kind() == reflect.Struct {
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
- if field.Type().Kind() == reflect.Ptr && field.IsNil() && v.Type().Field(i).Anonymous == true {
+ if field.Type().Kind() == reflect.Ptr && field.IsNil() && v.Type().Field(i).Anonymous {
field.Set(reflect.New(field.Type().Elem()))
}
}
@@ -219,6 +314,10 @@ func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values
// Slice of structs. Let's go recursive.
if len(parts) > 1 {
idx := parts[0].index
+ // a defensive check to avoid creating a large slice based on user input index
+ if idx > d.maxSize {
+ return fmt.Errorf("%v index %d is larger than the configured maxSize %d", v.Kind(), idx, d.maxSize)
+ }
if v.IsNil() || v.Len() < idx+1 {
value := reflect.MakeSlice(t, idx+1, idx+1)
if v.Len() < idx+1 {
@@ -93,8 +93,11 @@ func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error {
}
// Encode struct pointer types if the field is a valid pointer and a struct.
- if isValidStructPointer(v.Field(i)) {
- e.encode(v.Field(i).Elem(), dst)
+ if isValidStructPointer(v.Field(i)) && !e.hasCustomEncoder(v.Field(i).Type()) {
+ err := e.encode(v.Field(i).Elem(), dst)
+ if err != nil {
+ errors[v.Field(i).Elem().Type().String()] = err
+ }
continue
}
@@ -112,7 +115,10 @@ func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error {
}
if v.Field(i).Type().Kind() == reflect.Struct {
- e.encode(v.Field(i), dst)
+ err := e.encode(v.Field(i), dst)
+ if err != nil {
+ errors[v.Field(i).Type().String()] = err
+ }
continue
}
@@ -142,6 +148,11 @@ func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error {
return nil
}
+func (e *Encoder) hasCustomEncoder(t reflect.Type) bool {
+ _, exists := e.regenc[t]
+ return exists
+}
+
func typeEncoder(t reflect.Type, reg map[reflect.Type]encoderFunc) encoderFunc {
if f, ok := reg[t]; ok {
return f
@@ -673,8 +673,8 @@ github.com/gorilla/handlers
# github.com/gorilla/mux v1.8.1
## explicit; go 1.20
github.com/gorilla/mux
-# github.com/gorilla/schema v1.2.0
-## explicit
+# github.com/gorilla/schema v1.4.1
+## explicit; go 1.20
github.com/gorilla/schema
# github.com/hashicorp/errwrap v1.1.0
## explicit