do not allow backsteps in host volume plugin

Fixes #47107
pull/6/head
Josh Horwitz 2017-06-10 09:48:42 -04:00
parent 0a1b7d94b4
commit 48b3fb84ab
10 changed files with 249 additions and 62 deletions

View File

@ -20,8 +20,8 @@ import (
"encoding/json"
"fmt"
"net"
"os"
"path"
"path/filepath"
"reflect"
"regexp"
"strconv"
@ -627,7 +627,10 @@ func validateHostPathVolumeSource(hostPath *api.HostPathVolumeSource, fldPath *f
allErrs := field.ErrorList{}
if len(hostPath.Path) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("path"), ""))
return allErrs
}
allErrs = append(allErrs, validatePathNoBacksteps(hostPath.Path, fldPath.Child("path"))...)
return allErrs
}
@ -958,8 +961,18 @@ func validateLocalDescendingPath(targetPath string, fldPath *field.Path) field.E
allErrs = append(allErrs, field.Invalid(fldPath, targetPath, "must be a relative path"))
}
// TODO: this assumes the OS of apiserver & nodes are the same
parts := strings.Split(targetPath, string(os.PathSeparator))
allErrs = append(allErrs, validatePathNoBacksteps(targetPath, fldPath)...)
return allErrs
}
// validatePathNoBacksteps makes sure the targetPath does not have any `..` path elements when split
//
// This assumes the OS of the apiserver and the nodes are the same. The same check should be done
// on the node to ensure there are no backsteps.
func validatePathNoBacksteps(targetPath string, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
parts := strings.Split(filepath.ToSlash(targetPath), "/")
for _, item := range parts {
if item == ".." {
allErrs = append(allErrs, field.Invalid(fldPath, targetPath, "must not contain '..'"))

View File

@ -268,6 +268,19 @@ func TestValidatePersistentVolumes(t *testing.T) {
StorageClassName: "test-storage-class",
}),
},
"bad-hostpath-volume-backsteps": {
isExpectedFailure: true,
volume: testVolume("foo", "", api.PersistentVolumeSpec{
Capacity: api.ResourceList{
api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
},
AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
PersistentVolumeSource: api.PersistentVolumeSource{
HostPath: &api.HostPathVolumeSource{Path: "/foo/.."},
},
StorageClassName: "backstep-hostpath",
}),
},
}
for name, scenario := range scenarios {
@ -1102,6 +1115,20 @@ func TestValidateVolumes(t *testing.T) {
},
},
},
{
name: "invalid HostPath backsteps",
vol: api.Volume{
Name: "hostpath",
VolumeSource: api.VolumeSource{
HostPath: &api.HostPathVolumeSource{
Path: "/mnt/path/..",
},
},
},
errtype: field.ErrorTypeInvalid,
errfield: "path",
errdetail: "must not contain '..'",
},
// GcePersistentDisk
{
name: "valid GcePersistentDisk",

View File

@ -111,6 +111,7 @@ go_library(
"//pkg/volume/util:go_default_library",
"//pkg/volume/util/types:go_default_library",
"//pkg/volume/util/volumehelper:go_default_library",
"//pkg/volume/validation:go_default_library",
"//plugin/pkg/scheduler/algorithm:go_default_library",
"//plugin/pkg/scheduler/algorithm/predicates:go_default_library",
"//third_party/forked/golang/expansion:go_default_library",

View File

@ -61,6 +61,7 @@ import (
"k8s.io/kubernetes/pkg/util"
"k8s.io/kubernetes/pkg/volume"
"k8s.io/kubernetes/pkg/volume/util/volumehelper"
volumevalidation "k8s.io/kubernetes/pkg/volume/validation"
"k8s.io/kubernetes/third_party/forked/golang/expansion"
)
@ -138,6 +139,15 @@ func makeMounts(pod *v1.Pod, podDir string, container *v1.Container, hostName, h
return nil, err
}
if mount.SubPath != "" {
if filepath.IsAbs(mount.SubPath) {
return nil, fmt.Errorf("error SubPath `%s` must not be an absolute path", mount.SubPath)
}
err = volumevalidation.ValidatePathNoBacksteps(mount.SubPath)
if err != nil {
return nil, fmt.Errorf("unable to provision SubPath `%s`: %v", mount.SubPath, err)
}
fileinfo, err := os.Lstat(hostPath)
if err != nil {
return nil, err

View File

@ -42,76 +42,137 @@ import (
)
func TestMakeMounts(t *testing.T) {
container := v1.Container{
VolumeMounts: []v1.VolumeMount{
{
MountPath: "/etc/hosts",
Name: "disk",
ReadOnly: false,
testCases := map[string]struct {
container v1.Container
podVolumes kubecontainer.VolumeMap
expectErr bool
expectedErrMsg string
expectedMounts []kubecontainer.Mount
}{
"valid mounts": {
podVolumes: kubecontainer.VolumeMap{
"disk": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/disk"}},
"disk4": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/host"}},
"disk5": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/var/lib/kubelet/podID/volumes/empty/disk5"}},
},
{
MountPath: "/mnt/path3",
Name: "disk",
ReadOnly: true,
container: v1.Container{
VolumeMounts: []v1.VolumeMount{
{
MountPath: "/etc/hosts",
Name: "disk",
ReadOnly: false,
},
{
MountPath: "/mnt/path3",
Name: "disk",
ReadOnly: true,
},
{
MountPath: "/mnt/path4",
Name: "disk4",
ReadOnly: false,
},
{
MountPath: "/mnt/path5",
Name: "disk5",
ReadOnly: false,
},
},
},
{
MountPath: "/mnt/path4",
Name: "disk4",
ReadOnly: false,
expectedMounts: []kubecontainer.Mount{
{
Name: "disk",
ContainerPath: "/etc/hosts",
HostPath: "/mnt/disk",
ReadOnly: false,
SELinuxRelabel: false,
},
{
Name: "disk",
ContainerPath: "/mnt/path3",
HostPath: "/mnt/disk",
ReadOnly: true,
SELinuxRelabel: false,
},
{
Name: "disk4",
ContainerPath: "/mnt/path4",
HostPath: "/mnt/host",
ReadOnly: false,
SELinuxRelabel: false,
},
{
Name: "disk5",
ContainerPath: "/mnt/path5",
HostPath: "/var/lib/kubelet/podID/volumes/empty/disk5",
ReadOnly: false,
SELinuxRelabel: false,
},
},
{
MountPath: "/mnt/path5",
Name: "disk5",
ReadOnly: false,
expectErr: false,
},
"invalid absolute SubPath": {
podVolumes: kubecontainer.VolumeMap{
"disk": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/disk"}},
},
container: v1.Container{
VolumeMounts: []v1.VolumeMount{
{
MountPath: "/mnt/path3",
SubPath: "/must/not/be/absolute",
Name: "disk",
ReadOnly: true,
},
},
},
expectErr: true,
expectedErrMsg: "error SubPath `/must/not/be/absolute` must not be an absolute path",
},
"invalid SubPath with backsteps": {
podVolumes: kubecontainer.VolumeMap{
"disk": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/disk"}},
},
container: v1.Container{
VolumeMounts: []v1.VolumeMount{
{
MountPath: "/mnt/path3",
SubPath: "no/backsteps/../allowed",
Name: "disk",
ReadOnly: true,
},
},
},
expectErr: true,
expectedErrMsg: "unable to provision SubPath `no/backsteps/../allowed`: must not contain '..'",
},
}
podVolumes := kubecontainer.VolumeMap{
"disk": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/disk"}},
"disk4": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/host"}},
"disk5": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/var/lib/kubelet/podID/volumes/empty/disk5"}},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
pod := v1.Pod{
Spec: v1.PodSpec{
HostNetwork: true,
},
}
pod := v1.Pod{
Spec: v1.PodSpec{
HostNetwork: true,
},
}
mounts, err := makeMounts(&pod, "/pod", &tc.container, "fakepodname", "", "", tc.podVolumes)
mounts, _ := makeMounts(&pod, "/pod", &container, "fakepodname", "", "", podVolumes)
// validate only the error if we expect an error
if tc.expectErr {
if err == nil || err.Error() != tc.expectedErrMsg {
t.Fatalf("expected error message `%s` but got `%v`", tc.expectedErrMsg, err)
}
return
}
expectedMounts := []kubecontainer.Mount{
{
Name: "disk",
ContainerPath: "/etc/hosts",
HostPath: "/mnt/disk",
ReadOnly: false,
SELinuxRelabel: false,
},
{
Name: "disk",
ContainerPath: "/mnt/path3",
HostPath: "/mnt/disk",
ReadOnly: true,
SELinuxRelabel: false,
},
{
Name: "disk4",
ContainerPath: "/mnt/path4",
HostPath: "/mnt/host",
ReadOnly: false,
SELinuxRelabel: false,
},
{
Name: "disk5",
ContainerPath: "/mnt/path5",
HostPath: "/var/lib/kubelet/podID/volumes/empty/disk5",
ReadOnly: false,
SELinuxRelabel: false,
},
// otherwise validate the mounts
if err != nil {
t.Fatal(err)
}
assert.Equal(t, tc.expectedMounts, mounts, "mounts of container %+v", tc.container)
})
}
assert.Equal(t, expectedMounts, mounts, "mounts of container %+v", container)
}
func TestHostsFileContent(t *testing.T) {

View File

@ -19,6 +19,7 @@ go_library(
"//pkg/api/v1:go_default_library",
"//pkg/volume:go_default_library",
"//pkg/volume/util/volumehelper:go_default_library",
"//pkg/volume/validation:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/uuid:go_default_library",

View File

@ -27,6 +27,7 @@ import (
"k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/volume"
"k8s.io/kubernetes/pkg/volume/util/volumehelper"
"k8s.io/kubernetes/pkg/volume/validation"
)
// This is the primary entrypoint for volume plugins.
@ -103,6 +104,7 @@ func (plugin *hostPathPlugin) NewMounter(spec *volume.Spec, pod *v1.Pod, _ volum
if err != nil {
return nil, err
}
return &hostPathMounter{
hostPath: &hostPath{path: hostPathVolumeSource.Path},
readOnly: readOnly,
@ -205,6 +207,10 @@ func (b *hostPathMounter) CanMount() error {
// SetUp does nothing.
func (b *hostPathMounter) SetUp(fsGroup *types.UnixGroupID) error {
err := validation.ValidatePathNoBacksteps(b.GetPath())
if err != nil {
return fmt.Errorf("invalid HostPath `%s`: %v", b.GetPath(), err)
}
return nil
}

View File

@ -182,6 +182,31 @@ func TestProvisioner(t *testing.T) {
os.RemoveAll(pv.Spec.HostPath.Path)
}
func TestInvalidHostPath(t *testing.T) {
plugMgr := volume.VolumePluginMgr{}
plugMgr.InitPlugins(ProbeVolumePlugins(volume.VolumeConfig{}), volumetest.NewFakeVolumeHost("fake", nil, nil))
plug, err := plugMgr.FindPluginByName(hostPathPluginName)
if err != nil {
t.Fatalf("Unable to find plugin %s by name: %v", hostPathPluginName, err)
}
spec := &v1.Volume{
Name: "vol1",
VolumeSource: v1.VolumeSource{HostPath: &v1.HostPathVolumeSource{Path: "/no/backsteps/allowed/.."}},
}
pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{UID: types.UID("poduid")}}
mounter, err := plug.NewMounter(volume.NewSpecFromVolume(spec), pod, volume.VolumeOptions{})
if err != nil {
t.Fatal(err)
}
err = mounter.SetUp(nil)
expectedMsg := "invalid HostPath `/no/backsteps/allowed/..`: must not contain '..'"
if err.Error() != expectedMsg {
t.Fatalf("expected error `%s` but got `%s`", expectedMsg, err)
}
}
func TestPlugin(t *testing.T) {
plugMgr := volume.VolumePluginMgr{}
plugMgr.InitPlugins(ProbeVolumePlugins(volume.VolumeConfig{}), volumetest.NewFakeVolumeHost("fake", nil, nil))

View File

@ -17,6 +17,10 @@ limitations under the License.
package validation
import (
"errors"
"path/filepath"
"strings"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kubernetes/pkg/api"
)
@ -53,3 +57,15 @@ func checkMountOption(pv *api.PersistentVolume) field.ErrorList {
}
return allErrs
}
// ValidatePathNoBacksteps will make sure the targetPath does not have any element which is ".."
func ValidatePathNoBacksteps(targetPath string) error {
parts := strings.Split(filepath.ToSlash(targetPath), "/")
for _, item := range parts {
if item == ".." {
return errors.New("must not contain '..'")
}
}
return nil
}

View File

@ -84,3 +84,30 @@ func testVolumeWithMountOption(name string, namespace string, mountOptions strin
Spec: spec,
}
}
func TestValidatePathNoBacksteps(t *testing.T) {
testCases := map[string]struct {
path string
expectedErr bool
}{
"valid path": {
path: "/foo/bar",
},
"invalid path": {
path: "/foo/bar/..",
expectedErr: true,
},
}
for name, tc := range testCases {
err := ValidatePathNoBacksteps(tc.path)
if err == nil && tc.expectedErr {
t.Fatalf("expected test `%s` to return an error but it didnt", name)
}
if err != nil && !tc.expectedErr {
t.Fatalf("expected test `%s` to return no error but got `%v`", name, err)
}
}
}