CVE-2020-15186 (Go, published 2020-09-17T22:15Z)
Description: value: In Helm before versions 2.16.11 and 3.3.2 plugin names are not sanitized properly. As a result, a malicious plugin author could use characters in a plugin name that would result in unexpected behavior, such as duplicating the name of another plugin or spoofing the output to `helm --help`. This issue has been patched in Helm 3.3.2. A possible workaround is to not install untrusted Helm plugins. Examine the `name` field in the `plugin.yaml` file for a plugin, looking for characters outside of the [a-zA-Z0-9._-] range.
Severity: MEDIUM (CVSS v3 base score: 2.7)
Weakness: CWE-74 \u2014 Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
Repository: https://github.com/helm/helm
Files changed:
cmd/helm/load_plugins.go
cmd/helm/plugin_install.go
pkg/plugin/plugin.go
pkg/plugin/plugin_test.go
Commit message: Merge pull request from GHSA-m54r-vrmv-hw33

Signed-off-by: Matt Butcher <matt.butcher@microsoft.com>

Vulnerable code:
		fmt.Fprintf(os.Stderr, "failed to load plugins: %s", err)
		return err
	data, err := ioutil.ReadFile(filepath.Join(dirname, PluginFileName))
		return nil, err
		return nil, err
	return plug, nil
		return plugins, err
	return plugins, nil

Fixed code:
		fmt.Fprintf(os.Stderr, "failed to load plugins: %s\n", err)
	"github.com/pkg/errors"
		return errors.Wrap(err, "plugin is installed but unusable")
	"regexp"
	"github.com/pkg/errors"
// validPluginName is a regular expression that validates plugin names.
//
// Plugin names can only contain the ASCII characters a-z, A-Z, 0-9, ​_​ and ​-.
var validPluginName = regexp.MustCompile("^[A-Za-z0-9_-]+$")

// validatePluginData validates a plugin's YAML data.
func validatePluginData(plug *Plugin, filepath string) error {
	if !validPluginName.MatchString(plug.Metadata.Name) {
		return fmt.Errorf("invalid plugin name at %q", filepath)
	}
	// We could also validate SemVer, executable, and other fields should we so choose.
	return nil
}

func detectDuplicates(plugs []*Plugin) error {
	names := map[string]string{}

	for _, plug := range plugs {
		if oldpath, ok := names[plug.Metadata.Name]; ok {
			return fmt.Errorf(
				"two plugins claim the name %q at %q and %q",
				plug.Metadata.Name,
				oldpath,
				plug.Dir,
			)
		}
		names[plug.Metadata.Name] = plug.Dir
	}

	return nil
}

	pluginfile := filepath.Join(dirname, PluginFileName)
	data, err := ioutil.ReadFile(pluginfile)
		return nil, errors.Wrapf(err, "failed to read plugin at %q", pluginfile)
		return nil, errors.Wrapf(err, "failed to load plugin at %q", pluginfile)
	return plug, validatePluginData(plug, pluginfile)
		return plugins, errors.Wrapf(err, "failed to find plugins in %q", scanpath)
	return plugins, detectDuplicates(plugins)
	"fmt"

func TestValidatePluginData(t *testing.T) {
	for i, item := range []struct {
		pass bool
		plug *Plugin
	}{
		{true, mockPlugin("abcdefghijklmnopqrstuvwxyz0123456789_-ABC")},
		{true, mockPlugin("foo-bar-FOO-BAR_1234")},
		{false, mockPlugin("foo -bar")},
		{false, mockPlugin("$foo -bar")}, // Test leading chars
		{false, mockPlugin("foo -bar ")}, // Test trailing chars
		{false, mockPlugin("foo\nbar")},  // Test newline
	} {
		err := validatePluginData(item.plug, fmt.Sprintf("test-%d", i))
		if item.pass && err != nil {
			t.Errorf("failed to validate case %d: %s", i, err)
		} else if !item.pass && err == nil {
			t.Errorf("expected case %d to fail", i)
		}
	}
}

func TestDetectDuplicates(t *testing.T) {
	plugs := []*Plugin{
		mockPlugin("foo"),
		mockPlugin("bar"),
	}
	if err := detectDuplicates(plugs); err != nil {
		t.Error("no duplicates in the first set")
	}
	plugs = append(plugs, mockPlugin("foo"))
	if err := detectDuplicates(plugs); err == nil {
		t.Error("duplicates in the second set")
	}
}

func mockPlugin(name string) *Plugin {
	return &Plugin{
		Metadata: &Metadata{
			Name:        name,
			Version:     "v0.1.2",
			Usage:       "Mock plugin",
			Description: "Mock plugin for testing",
			Command:     "echo mock plugin",
		},
		Dir: "no-such-dir",
	}
}