From d6cf1d454924f8d7574649f032b70718aa80df56 Mon Sep 17 00:00:00 2001 From: Matt Heon Date: Wed, 3 Jul 2024 14:34:48 -0400 Subject: [PATCH] Update gorilla/schema to v1.4.1 to fix CVE-2024-37298 Ref: OCPBUGS-36439 Signed-off-by: Matt Heon --- 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 diff --git a/go.mod b/go.mod index 844a7de207..f39d43038a 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/google/uuid v1.3.0 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 - github.com/gorilla/schema v1.2.0 + github.com/gorilla/schema v1.4.1 github.com/hashicorp/go-multierror v1.1.1 github.com/json-iterator/go v1.1.12 github.com/mattn/go-shellwords v1.0.12 diff --git a/go.sum b/go.sum index 4387b58f87..7b4185a413 100644 --- a/go.sum +++ b/go.sum @@ -625,8 +625,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.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -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= diff --git a/vendor/github.com/gorilla/schema/.editorconfig b/vendor/github.com/gorilla/schema/.editorconfig new file mode 100644 index 0000000000..c6b74c3e0d --- /dev/null +++ b/vendor/github.com/gorilla/schema/.editorconfig @@ -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 diff --git a/vendor/github.com/gorilla/schema/.gitignore b/vendor/github.com/gorilla/schema/.gitignore new file mode 100644 index 0000000000..84039fec68 --- /dev/null +++ b/vendor/github.com/gorilla/schema/.gitignore @@ -0,0 +1 @@ +coverage.coverprofile diff --git a/vendor/github.com/gorilla/schema/LICENSE b/vendor/github.com/gorilla/schema/LICENSE index 0e5fb87280..bb9d80bc9b 100644 --- a/vendor/github.com/gorilla/schema/LICENSE +++ b/vendor/github.com/gorilla/schema/LICENSE @@ -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 diff --git a/vendor/github.com/gorilla/schema/Makefile b/vendor/github.com/gorilla/schema/Makefile new file mode 100644 index 0000000000..98f5ab75f9 --- /dev/null +++ b/vendor/github.com/gorilla/schema/Makefile @@ -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 diff --git a/vendor/github.com/gorilla/schema/README.md b/vendor/github.com/gorilla/schema/README.md index aefdd66996..58786ba502 100644 --- a/vendor/github.com/gorilla/schema/README.md +++ b/vendor/github.com/gorilla/schema/README.md @@ -1,8 +1,12 @@ -schema -====== -[![GoDoc](https://godoc.org/github.com/gorilla/schema?status.svg)](https://godoc.org/github.com/gorilla/schema) [![Build Status](https://travis-ci.org/gorilla/schema.png?branch=master)](https://travis-ci.org/gorilla/schema) -[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/schema/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/schema?badge) +# gorilla/schema +![testing](https://github.com/gorilla/schema/actions/workflows/test.yml/badge.svg) +[![codecov](https://codecov.io/github/gorilla/schema/branch/main/graph/badge.svg)](https://codecov.io/github/gorilla/schema) +[![godoc](https://godoc.org/github.com/gorilla/schema?status.svg)](https://godoc.org/github.com/gorilla/schema) +[![sourcegraph](https://sourcegraph.com/github.com/gorilla/schema/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/schema?badge) + + +![Gorilla Logo](https://github.com/gorilla/.github/assets/53367916/d92caabf-98e0-473e-bfbf-ab554ba435e5) 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 diff --git a/vendor/github.com/gorilla/schema/cache.go b/vendor/github.com/gorilla/schema/cache.go index 0746c1202c..065b8d6ee6 100644 --- a/vendor/github.com/gorilla/schema/cache.go +++ b/vendor/github.com/gorilla/schema/cache.go @@ -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 "" +} diff --git a/vendor/github.com/gorilla/schema/converter.go b/vendor/github.com/gorilla/schema/converter.go index 4f2116a15e..4bae6df962 100644 --- a/vendor/github.com/gorilla/schema/converter.go +++ b/vendor/github.com/gorilla/schema/converter.go @@ -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 +} diff --git a/vendor/github.com/gorilla/schema/decoder.go b/vendor/github.com/gorilla/schema/decoder.go index 025e438b56..54c88ecb34 100644 --- a/vendor/github.com/gorilla/schema/decoder.go +++ b/vendor/github.com/gorilla/schema/decoder.go @@ -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 { diff --git a/vendor/github.com/gorilla/schema/encoder.go b/vendor/github.com/gorilla/schema/encoder.go index f0ed631210..52f2c108e4 100644 --- a/vendor/github.com/gorilla/schema/encoder.go +++ b/vendor/github.com/gorilla/schema/encoder.go @@ -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 diff --git a/vendor/modules.txt b/vendor/modules.txt index b8760120b8..ff45a64c3f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -555,8 +555,8 @@ github.com/gorilla/handlers # github.com/gorilla/mux v1.8.0 ## explicit; go 1.12 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