From a3eed35c0db3f9bd9e960b8cd619a2c3ae6c2546 Mon Sep 17 00:00:00 2001 From: Skye Gill Date: Thu, 19 Jan 2023 10:52:11 +0000 Subject: [PATCH 1/2] fix: validate that a flag key is valid UTF-8 & implemented fuzzing tests Signed-off-by: Skye Gill --- README.md | 16 +++- integration/evaluation_fuzz_test.go | 116 ++++++++++++++++++++++++++++ pkg/openfeature/client.go | 20 +++-- 3 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 integration/evaluation_fuzz_test.go diff --git a/README.md b/README.md index 6714a309..d2abd6dc 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ Run unit tests with `make test`. #### Integration tests -The continuous integration runs a set of [gherkin integration tests](https://github.com/open-feature/test-harness/blob/main/features) using [`flagd`](https://github.com/open-feature/flagd). +The continuous integration runs a set of [gherkin integration tests](https://github.com/open-feature/test-harness/blob/main/features) using the [flagd provider](https://github.com/open-feature/go-sdk-contrib/tree/main/providers/flagd) and [flagd](https://github.com/open-feature/flagd). If you'd like to run them locally, first pull the `test-harness` git submodule ``` git submodule update --init --recursive @@ -119,6 +119,20 @@ docker run -p 8013:8013 -v $PWD/test-harness/testing-flags.json:/testing-flags.j make integration-test ``` +#### Fuzzing + +[Go supports fuzzing natively as of 1.18](https://go.dev/security/fuzz/). +The fuzzing suite is implemented as an integration of `go-sdk` with the [flagd provider](https://github.com/open-feature/go-sdk-contrib/tree/main/providers/flagd) and [flagd](https://github.com/open-feature/flagd). +The fuzzing tests are found in [./integration/evaluation_fuzz_test.go](./integration/evaluation_fuzz_test.go), they are dependent on the flagd testbed running, you can start it with +``` +docker run -p 8013:8013 ghcr.io/open-feature/flagd-testbed:latest +``` +then, to execute a fuzzing test, run the following +``` +go test -fuzz=FuzzBooleanEvaluation ./integration/evaluation_fuzz_test.go +``` +substituting the name of the fuzz as appropriate. + ### Releases This repo uses Release Please to release packages. Release Please sets up a running PR that tracks all changes for the library components, and maintains the versions according to conventional commits, generated when PRs are merged. When Release Please's running PR is merged, any changed artifacts are published. diff --git a/integration/evaluation_fuzz_test.go b/integration/evaluation_fuzz_test.go new file mode 100644 index 00000000..00b8eabf --- /dev/null +++ b/integration/evaluation_fuzz_test.go @@ -0,0 +1,116 @@ +package integration_test + +import ( + "context" + "strings" + "testing" + "time" + + flagd "github.com/open-feature/go-sdk-contrib/providers/flagd/pkg" + "github.com/open-feature/go-sdk/pkg/openfeature" +) + +func setupFuzzClient(f *testing.F) *openfeature.Client { + f.Helper() + + provider := flagd.NewProvider(flagd.WithPort(8013), flagd.WithoutCache()) + openfeature.SetProvider(provider) + + select { + case <-provider.IsReady(): + case <-time.After(500 * time.Millisecond): + f.Fatal("provider not ready after 500 milliseconds") + } + + return openfeature.NewClient("fuzzing") +} + +func FuzzBooleanEvaluation(f *testing.F) { + client := setupFuzzClient(f) + + f.Add("foo", false) + f.Fuzz(func(t *testing.T, flagKey string, defaultValue bool) { + res, err := client.BooleanValueDetails(context.Background(), flagKey, defaultValue, openfeature.EvaluationContext{}) + if err != nil { + if res.ErrorCode == openfeature.FlagNotFoundCode { + return + } + if strings.Contains(err.Error(), string(openfeature.ParseErrorCode)) { + return + } + t.Error(err) + } + }) +} + +func FuzzStringEvaluation(f *testing.F) { + client := setupFuzzClient(f) + + f.Add("foo", "bar") + f.Fuzz(func(t *testing.T, flagKey string, defaultValue string) { + res, err := client.StringValueDetails(context.Background(), flagKey, defaultValue, openfeature.EvaluationContext{}) + if err != nil { + if res.ErrorCode == openfeature.FlagNotFoundCode { + return + } + if strings.Contains(err.Error(), string(openfeature.ParseErrorCode)) { + return + } + t.Error(err) + } + }) +} + +func FuzzIntEvaluation(f *testing.F) { + client := setupFuzzClient(f) + + f.Add("foo", int64(1)) + f.Fuzz(func(t *testing.T, flagKey string, defaultValue int64) { + res, err := client.IntValueDetails(context.Background(), flagKey, defaultValue, openfeature.EvaluationContext{}) + if err != nil { + if res.ErrorCode == openfeature.FlagNotFoundCode { + return + } + if strings.Contains(err.Error(), string(openfeature.ParseErrorCode)) { + return + } + t.Error(err) + } + }) +} + +func FuzzFloatEvaluation(f *testing.F) { + client := setupFuzzClient(f) + + f.Add("foo", float64(1)) + f.Fuzz(func(t *testing.T, flagKey string, defaultValue float64) { + res, err := client.FloatValueDetails(context.Background(), flagKey, defaultValue, openfeature.EvaluationContext{}) + if err != nil { + if res.ErrorCode == openfeature.FlagNotFoundCode { + return + } + if strings.Contains(err.Error(), string(openfeature.ParseErrorCode)) { + return + } + t.Error(err) + } + }) +} + +func FuzzObjectEvaluation(f *testing.F) { + client := setupFuzzClient(f) + + f.Add("foo", "{}") + f.Fuzz(func(t *testing.T, flagKey string, defaultValue string) { // interface{} is not supported, using a string + res, err := client.ObjectValueDetails(context.Background(), flagKey, defaultValue, openfeature.EvaluationContext{}) + if err != nil { + if res.ErrorCode == openfeature.FlagNotFoundCode { + return + } + if strings.Contains(err.Error(), string(openfeature.ParseErrorCode)) { + return + } + t.Error(err) + } + }) +} diff --git a/pkg/openfeature/client.go b/pkg/openfeature/client.go index 362a1b51..23cefc2b 100644 --- a/pkg/openfeature/client.go +++ b/pkg/openfeature/client.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "sync" + "unicode/utf8" "github.com/go-logr/logr" ) @@ -563,6 +564,18 @@ func (c *Client) evaluate( "evaluationContext", evalCtx, "evaluationOptions", options, ) + evalDetails := InterfaceEvaluationDetails{ + Value: defaultValue, + EvaluationDetails: EvaluationDetails{ + FlagKey: flag, + FlagType: flagType, + }, + } + + if !utf8.Valid([]byte(flag)) { + return evalDetails, NewParseErrorResolutionError("flag key is not a UTF-8 encoded string") + } + // ensure that the same provider & hooks are used across this transaction to avoid unexpected behaviour api.RLock() provider := api.prvder @@ -583,13 +596,6 @@ func (c *Client) evaluate( providerMetadata: provider.Metadata(), evaluationContext: evalCtx, } - evalDetails := InterfaceEvaluationDetails{ - Value: defaultValue, - EvaluationDetails: EvaluationDetails{ - FlagKey: flag, - FlagType: flagType, - }, - } defer func() { c.finallyHooks(hookCtx, providerInvocationClientApiHooks, options) From 6a5ff5dca6071bafef983c8bff17e956f6399ae4 Mon Sep 17 00:00:00 2001 From: Skye Gill Date: Fri, 27 Jan 2023 11:05:38 +0000 Subject: [PATCH 2/2] added more fuzz seeds Signed-off-by: Skye Gill --- go.mod | 1 - go.sum | 15 ++------------- integration/evaluation_fuzz_test.go | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 8bcf2d50..87cd5618 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,6 @@ require ( github.com/prometheus/procfs v0.8.0 // indirect github.com/rs/cors v1.8.3 // indirect github.com/rs/xid v1.4.0 // indirect - github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect diff --git a/go.sum b/go.sum index 8b85d751..2b938390 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,6 @@ github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/diegoholiveira/jsonlogic/v3 v3.2.6 h1:EV607wRY72hT3V90ZOQw+zjXR9KIUV9jnHfT3yS8uks= -github.com/diegoholiveira/jsonlogic/v3 v3.2.6/go.mod h1:9oE8z9G+0OMxOoLHF3fhek3KuqD5CBqM0B6XFL08MSg= github.com/diegoholiveira/jsonlogic/v3 v3.2.7 h1:awX07pFPnlntZzRNBcO4a2Ivxa77NMt+narq/6xcS0E= github.com/diegoholiveira/jsonlogic/v3 v3.2.7/go.mod h1:9oE8z9G+0OMxOoLHF3fhek3KuqD5CBqM0B6XFL08MSg= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -207,14 +205,8 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/open-feature/flagd v0.3.1 h1:8MhBcNPWB9k1/y+HTnNnMAlQd+vLApFSuCf6Jx++xdg= -github.com/open-feature/flagd v0.3.1/go.mod h1:r/8ojycJpCzXINl7Y5KrSx2qu+fsEUA2TxmxVQnHO7s= github.com/open-feature/flagd v0.3.2 h1:Xfmtd8z2LQ80ux2nwymNx8GrLebsLfbtDF3bFC1nVVM= github.com/open-feature/flagd v0.3.2/go.mod h1:Xlle3jks+TBtZ6MKl/AiAeSB5FocSEwdq5CjO1O5eUs= -github.com/open-feature/go-sdk-contrib/providers/flagd v0.1.2 h1:RxuWl1D9MJJIzOpDA2cue0g9hmgMX8Nt4eV6jBt/+4E= -github.com/open-feature/go-sdk-contrib/providers/flagd v0.1.2/go.mod h1:6wbRT4QeBJXkfyWnntXZbxm1noDTVEmDUmzrCW5kbEA= -github.com/open-feature/go-sdk-contrib/providers/flagd v0.1.3 h1:jT9WOr7hfDHgwxsfZoS07Mm9FxXJdw2TiZ2z5TOJA1k= -github.com/open-feature/go-sdk-contrib/providers/flagd v0.1.3/go.mod h1:A4ZQULeRsddk5g2p/yrdH6mjZKH4xNp9DW3pEG4Z8fs= github.com/open-feature/go-sdk-contrib/providers/flagd v0.1.4 h1:AkaY+pqcjHQe8W/ZFdfJpsPGsxoLyPRYHpTMIhiboAw= github.com/open-feature/go-sdk-contrib/providers/flagd v0.1.4/go.mod h1:5U1Ry0iFy4j466JafVdK210E7wo6YODKnoaREyhCiHo= github.com/open-feature/schemas v0.2.8 h1:oA75hJXpOd9SFgmNI2IAxWZkwzQPUDm7Jyyh3q489wM= @@ -260,8 +252,6 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= @@ -434,7 +424,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -548,7 +537,7 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 h1:a2S6M0+660BgMNl++4JPlcAO/CjkqYItDEZwkoDQK7c= +google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef h1:uQ2vjV/sHTsWSqdKeLqmwitzgvjMl7o4IdtHwUDXSJY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -561,7 +550,7 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.52.0 h1:kd48UiU7EHsV4rnLyOJRuP/Il/UHE7gdDAQ+SZI7nZk= +google.golang.org/grpc v1.52.3 h1:pf7sOysg4LdgBqduXveGKrcEwbStiK2rtfghdzlUYDQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/integration/evaluation_fuzz_test.go b/integration/evaluation_fuzz_test.go index 00b8eabf..c8afedc1 100644 --- a/integration/evaluation_fuzz_test.go +++ b/integration/evaluation_fuzz_test.go @@ -29,6 +29,9 @@ func FuzzBooleanEvaluation(f *testing.F) { client := setupFuzzClient(f) f.Add("foo", false) + f.Add("FoO", true) + f.Add("FoO234", false) + f.Add("FoO2\b34", true) f.Fuzz(func(t *testing.T, flagKey string, defaultValue bool) { res, err := client.BooleanValueDetails(context.Background(), flagKey, defaultValue, openfeature.EvaluationContext{}) if err != nil { @@ -47,6 +50,9 @@ func FuzzStringEvaluation(f *testing.F) { client := setupFuzzClient(f) f.Add("foo", "bar") + f.Add("FoO", "BaR") + f.Add("FoO234", "Ba1232") + f.Add("FoO2\b34", "BaaR\b2312") f.Fuzz(func(t *testing.T, flagKey string, defaultValue string) { res, err := client.StringValueDetails(context.Background(), flagKey, defaultValue, openfeature.EvaluationContext{}) if err != nil { @@ -65,6 +71,9 @@ func FuzzIntEvaluation(f *testing.F) { client := setupFuzzClient(f) f.Add("foo", int64(1)) + f.Add("FoO", int64(99)) + f.Add("FoO234", int64(100029)) + f.Add("FoO2\b34", int64(-1)) f.Fuzz(func(t *testing.T, flagKey string, defaultValue int64) { res, err := client.IntValueDetails(context.Background(), flagKey, defaultValue, openfeature.EvaluationContext{}) if err != nil { @@ -83,6 +92,9 @@ func FuzzFloatEvaluation(f *testing.F) { client := setupFuzzClient(f) f.Add("foo", float64(1)) + f.Add("FoO", 99.9) + f.Add("FoO234", 0.00004) + f.Add("FoO2\b34", -1.9203) f.Fuzz(func(t *testing.T, flagKey string, defaultValue float64) { res, err := client.FloatValueDetails(context.Background(), flagKey, defaultValue, openfeature.EvaluationContext{}) if err != nil { @@ -101,6 +113,9 @@ func FuzzObjectEvaluation(f *testing.F) { client := setupFuzzClient(f) f.Add("foo", "{}") + f.Add("FoO", "true") + f.Add("FoO234", "-1.23") + f.Add("FoO2\b34", "1") f.Fuzz(func(t *testing.T, flagKey string, defaultValue string) { // interface{} is not supported, using a string res, err := client.ObjectValueDetails(context.Background(), flagKey, defaultValue, openfeature.EvaluationContext{}) if err != nil {