harden config.Path() to disallow directory traversal

Signed-off-by: Nick Adcock <nick.adcock@docker.com>
This commit is contained in:
Nick Adcock 2019-03-07 14:28:42 +00:00
parent 79e1cabf17
commit ff51b0d77d
4 changed files with 82 additions and 19 deletions

View File

@ -35,15 +35,20 @@ func IsNotFound(err error) bool {
return ok return ok
} }
func getPluginDirs(dockerCli command.Cli) []string { func getPluginDirs(dockerCli command.Cli) ([]string, error) {
var pluginDirs []string var pluginDirs []string
if cfg := dockerCli.ConfigFile(); cfg != nil { if cfg := dockerCli.ConfigFile(); cfg != nil {
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...) pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
} }
pluginDirs = append(pluginDirs, config.Path("cli-plugins")) pluginDir, err := config.Path("cli-plugins")
if err != nil {
return nil, err
}
pluginDirs = append(pluginDirs, pluginDir)
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...) pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
return pluginDirs return pluginDirs, nil
} }
func addPluginCandidatesFromDir(res map[string][]string, d string) error { func addPluginCandidatesFromDir(res map[string][]string, d string) error {
@ -96,7 +101,12 @@ func listPluginCandidates(dirs []string) (map[string][]string, error) {
// ListPlugins produces a list of the plugins available on the system // ListPlugins produces a list of the plugins available on the system
func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) { func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) {
candidates, err := listPluginCandidates(getPluginDirs(dockerCli)) pluginDirs, err := getPluginDirs(dockerCli)
if err != nil {
return nil, err
}
candidates, err := listPluginCandidates(pluginDirs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -132,7 +142,12 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
return nil, errPluginNotFound(name) return nil, errPluginNotFound(name)
} }
exename := addExeSuffix(NamePrefix + name) exename := addExeSuffix(NamePrefix + name)
for _, d := range getPluginDirs(dockerCli) { pluginDirs, err := getPluginDirs(dockerCli)
if err != nil {
return nil, err
}
for _, d := range pluginDirs {
path := filepath.Join(d, exename) path := filepath.Join(d, exename)
// We stat here rather than letting the exec tell us // We stat here rather than letting the exec tell us

View File

@ -92,10 +92,14 @@ func TestErrPluginNotFound(t *testing.T) {
func TestGetPluginDirs(t *testing.T) { func TestGetPluginDirs(t *testing.T) {
cli := test.NewFakeCli(nil) cli := test.NewFakeCli(nil)
expected := []string{config.Path("cli-plugins")} pluginDir, err := config.Path("cli-plugins")
expected = append(expected, defaultSystemPluginDirs...) assert.NilError(t, err)
expected := append([]string{pluginDir}, defaultSystemPluginDirs...)
assert.Equal(t, strings.Join(expected, ":"), strings.Join(getPluginDirs(cli), ":")) var pluginDirs []string
pluginDirs, err = getPluginDirs(cli)
assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":"))
assert.NilError(t, err)
extras := []string{ extras := []string{
"foo", "bar", "baz", "foo", "bar", "baz",
@ -104,5 +108,7 @@ func TestGetPluginDirs(t *testing.T) {
cli.SetConfigFile(&configfile.ConfigFile{ cli.SetConfigFile(&configfile.ConfigFile{
CLIPluginsExtraDirs: extras, CLIPluginsExtraDirs: extras,
}) })
assert.DeepEqual(t, expected, getPluginDirs(cli)) pluginDirs, err = getPluginDirs(cli)
assert.DeepEqual(t, expected, pluginDirs)
assert.NilError(t, err)
} }

View File

@ -5,6 +5,7 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/credentials" "github.com/docker/cli/cli/config/credentials"
@ -43,12 +44,16 @@ func ContextStoreDir() string {
// SetDir sets the directory the configuration file is stored in // SetDir sets the directory the configuration file is stored in
func SetDir(dir string) { func SetDir(dir string) {
configDir = dir configDir = filepath.Clean(dir)
} }
// Path returns the path to a file relative to the config dir // Path returns the path to a file relative to the config dir
func Path(p ...string) string { func Path(p ...string) (string, error) {
return filepath.Join(append([]string{Dir()}, p...)...) path := filepath.Join(append([]string{Dir()}, p...)...)
if !strings.HasPrefix(path, Dir()+string(filepath.Separator)) {
return "", errors.Errorf("path %q is outside of root config directory %q", path, Dir())
}
return path, nil
} }
// LegacyLoadFromReader is a convenience function that creates a ConfigFile object from // LegacyLoadFromReader is a convenience function that creates a ConfigFile object from

View File

@ -2,6 +2,7 @@ package config
import ( import (
"bytes" "bytes"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
@ -552,13 +553,49 @@ func TestLoadDefaultConfigFile(t *testing.T) {
func TestConfigPath(t *testing.T) { func TestConfigPath(t *testing.T) {
oldDir := Dir() oldDir := Dir()
SetDir("dummy1") for _, tc := range []struct {
f1 := Path("a", "b") name string
assert.Equal(t, f1, filepath.Join("dummy1", "a", "b")) dir string
path []string
SetDir("dummy2") expected string
f2 := Path("c", "d") expectedErr string
assert.Equal(t, f2, filepath.Join("dummy2", "c", "d")) }{
{
name: "valid_path",
dir: "dummy",
path: []string{"a", "b"},
expected: filepath.Join("dummy", "a", "b"),
},
{
name: "valid_path_absolute_dir",
dir: "/dummy",
path: []string{"a", "b"},
expected: filepath.Join("/dummy", "a", "b"),
},
{
name: "invalid_relative_path",
dir: "dummy",
path: []string{"e", "..", "..", "f"},
expectedErr: fmt.Sprintf("is outside of root config directory %q", "dummy"),
},
{
name: "invalid_absolute_path",
dir: "dummy",
path: []string{"/a", "..", ".."},
expectedErr: fmt.Sprintf("is outside of root config directory %q", "dummy"),
},
} {
t.Run(tc.name, func(t *testing.T) {
SetDir(tc.dir)
f, err := Path(tc.path...)
assert.Equal(t, f, tc.expected)
if tc.expectedErr == "" {
assert.NilError(t, err)
} else {
assert.ErrorContains(t, err, tc.expectedErr)
}
})
}
SetDir(oldDir) SetDir(oldDir)
} }