From 9232535cf681dcb0ea9e7d412565f20395e16a40 Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Thu, 20 Nov 2025 10:50:04 +0100 Subject: [PATCH] Validate plugin module name Co-authored-by: Romain --- pkg/plugins/plugins.go | 11 ++--- pkg/plugins/plugins_test.go | 85 +++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 pkg/plugins/plugins_test.go diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 6888ceea4..16fcd7a94 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/go-multierror" "github.com/traefik/traefik/v2/pkg/log" + "golang.org/x/mod/module" ) const localGoPath = "./plugins-local/" @@ -68,24 +69,18 @@ func checkRemotePluginsConfiguration(plugins map[string]Descriptor) error { var errs []string for pAlias, descriptor := range plugins { - if descriptor.ModuleName == "" { - errs = append(errs, fmt.Sprintf("%s: plugin name is missing", pAlias)) + if err := module.CheckPath(descriptor.ModuleName); err != nil { + errs = append(errs, fmt.Sprintf("%s: malformed plugin module name is missing: %s", pAlias, err)) } if descriptor.Version == "" { errs = append(errs, fmt.Sprintf("%s: plugin version is missing", pAlias)) } - if strings.HasPrefix(descriptor.ModuleName, "/") || strings.HasSuffix(descriptor.ModuleName, "/") { - errs = append(errs, fmt.Sprintf("%s: plugin name should not start or end with a /", pAlias)) - continue - } - if _, ok := uniq[descriptor.ModuleName]; ok { errs = append(errs, fmt.Sprintf("only one version of a plugin is allowed, there is a duplicate of %s", descriptor.ModuleName)) continue } - uniq[descriptor.ModuleName] = struct{}{} } diff --git a/pkg/plugins/plugins_test.go b/pkg/plugins/plugins_test.go new file mode 100644 index 000000000..0fff67915 --- /dev/null +++ b/pkg/plugins/plugins_test.go @@ -0,0 +1,85 @@ +package plugins + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_checkRemotePluginsConfiguration(t *testing.T) { + testCases := []struct { + name string + plugins map[string]Descriptor + wantErr bool + }{ + { + name: "nil plugins configuration returns no error", + plugins: nil, + wantErr: false, + }, + { + name: "malformed module name returns error", + plugins: map[string]Descriptor{ + "plugin1": {ModuleName: "invalid/module/name", Version: "v1.0.0"}, + }, + wantErr: true, + }, + { + name: "malformed module name with path traversal returns error", + plugins: map[string]Descriptor{ + "plugin1": {ModuleName: "github.com/module/../name", Version: "v1.0.0"}, + }, + wantErr: true, + }, + { + name: "malformed module name with encoded path traversal returns error", + plugins: map[string]Descriptor{ + "plugin1": {ModuleName: "github.com/module%2F%2E%2E%2Fname", Version: "v1.0.0"}, + }, + wantErr: true, + }, + { + name: "malformed module name returns error", + plugins: map[string]Descriptor{ + "plugin1": {ModuleName: "invalid/module/name", Version: "v1.0.0"}, + }, + wantErr: true, + }, + { + name: "missing plugin version returns error", + plugins: map[string]Descriptor{ + "plugin1": {ModuleName: "github.com/module/name", Version: ""}, + }, + wantErr: true, + }, + { + name: "duplicate plugin module name returns error", + plugins: map[string]Descriptor{ + "plugin1": {ModuleName: "github.com/module/name", Version: "v1.0.0"}, + "plugin2": {ModuleName: "github.com/module/name", Version: "v1.1.0"}, + }, + wantErr: true, + }, + { + name: "valid plugins configuration returns no error", + plugins: map[string]Descriptor{ + "plugin1": {ModuleName: "github.com/module/name1", Version: "v1.0.0"}, + "plugin2": {ModuleName: "github.com/module/name2", Version: "v1.1.0"}, + }, + wantErr: false, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + err := checkRemotePluginsConfiguration(test.plugins) + if test.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +}