VYPR
Moderate severityNVD Advisory· Published Apr 1, 2019· Updated Sep 16, 2024

kubectl cp path traversal

CVE-2019-1002101

Description

The kubectl cp command allows copying files between containers and the user machine. To copy files from a container, Kubernetes creates a tar inside the container, copies it over the network, and kubectl unpacks it on the user’s machine. If the tar binary in the container is malicious, it could run any code and output unexpected, malicious results. An attacker could use this to write files to any path on the user’s machine when kubectl cp is called, limited only by the system permissions of the local user. The untar function can both create and follow symbolic links. The issue is resolved in kubectl v1.11.9, v1.12.7, v1.13.5, and v1.14.0.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
k8s.io/kubernetesGo
< 1.11.91.11.9
k8s.io/kubernetesGo
>= 1.12.0, < 1.12.71.12.7
k8s.io/kubernetesGo
>= 1.13.0, < 1.13.51.13.5

Affected products

1

Patches

1
47063891dd78

Merge pull request #75037 from soltysh/cp_bug

https://github.com/kubernetes/kubernetesKubernetes Prow RobotMar 6, 2019via ghsa
2 files changed · +97 19
  • pkg/kubectl/cmd/cp/cp.go+18 4 modified
    @@ -307,7 +307,7 @@ func (o *CopyOptions) copyFromPod(src, dest fileSpec) error {
     	// remove extraneous path shortcuts - these could occur if a path contained extra "../"
     	// and attempted to navigate beyond "/" in a remote filesystem
     	prefix = stripPathShortcuts(prefix)
    -	return untarAll(reader, dest.File, prefix)
    +	return o.untarAll(reader, dest.File, prefix)
     }
     
     // stripPathShortcuts removes any leading or trailing "../" from a given path
    @@ -418,7 +418,7 @@ func clean(fileName string) string {
     	return path.Clean(string(os.PathSeparator) + fileName)
     }
     
    -func untarAll(reader io.Reader, destFile, prefix string) error {
    +func (o *CopyOptions) untarAll(reader io.Reader, destFile, prefix string) error {
     	entrySeq := -1
     
     	// TODO: use compression here?
    @@ -433,6 +433,12 @@ func untarAll(reader io.Reader, destFile, prefix string) error {
     		}
     		entrySeq++
     		mode := header.FileInfo().Mode()
    +		// all the files will start with the prefix, which is the directory where
    +		// they were located on the pod, we need to strip down that prefix, but
    +		// if the prefix is missing it means the tar was tempered with
    +		if !strings.HasPrefix(header.Name, prefix) {
    +			return fmt.Errorf("tar contents corrupted")
    +		}
     		outFileName := path.Join(destFile, clean(header.Name[len(prefix):]))
     		baseName := path.Dir(outFileName)
     		if err := os.MkdirAll(baseName, 0755); err != nil {
    @@ -457,8 +463,16 @@ func untarAll(reader io.Reader, destFile, prefix string) error {
     		}
     
     		if mode&os.ModeSymlink != 0 {
    -			err := os.Symlink(header.Linkname, outFileName)
    -			if err != nil {
    +			linkname := header.Linkname
    +			// error is returned if linkname can't be made relative to destFile,
    +			// but relative can end up being ../dir that's why we also need to
    +			// verify if relative path is the same after Clean-ing
    +			relative, err := filepath.Rel(destFile, linkname)
    +			if path.IsAbs(linkname) && (err != nil || relative != stripPathShortcuts(relative)) {
    +				fmt.Fprintf(o.IOStreams.ErrOut, "warning: link %q is pointing to %q which is outside target destination, skipping\n", outFileName, header.Linkname)
    +				continue
    +			}
    +			if err := os.Symlink(linkname, outFileName); err != nil {
     				return err
     			}
     		} else {
    
  • pkg/kubectl/cmd/cp/cp_test.go+79 15 modified
    @@ -191,27 +191,33 @@ func TestStripPathShortcuts(t *testing.T) {
     	}
     }
     
    -func TestTarUntar(t *testing.T) {
    -	dir, err := ioutil.TempDir("", "input")
    -	dir2, err2 := ioutil.TempDir("", "output")
    -	if err != nil || err2 != nil {
    -		t.Errorf("unexpected error: %v | %v", err, err2)
    +func checkErr(t *testing.T, err error) {
    +	if err != nil {
    +		t.Errorf("unexpected error: %v", err)
     		t.FailNow()
     	}
    +}
    +
    +func TestTarUntar(t *testing.T) {
    +	dir, err := ioutil.TempDir("", "input")
    +	checkErr(t, err)
    +	dir2, err := ioutil.TempDir("", "output")
    +	checkErr(t, err)
    +	dir3, err := ioutil.TempDir("", "dir")
    +	checkErr(t, err)
    +
     	dir = dir + "/"
     	defer func() {
    -		if err := os.RemoveAll(dir); err != nil {
    -			t.Errorf("Unexpected error cleaning up: %v", err)
    -		}
    -		if err := os.RemoveAll(dir2); err != nil {
    -			t.Errorf("Unexpected error cleaning up: %v", err)
    -		}
    +		os.RemoveAll(dir)
    +		os.RemoveAll(dir2)
    +		os.RemoveAll(dir3)
     	}()
     
     	files := []struct {
     		name     string
     		nameList []string
     		data     string
    +		omitted  bool
     		fileType FileType
     	}{
     		{
    @@ -236,7 +242,24 @@ func TestTarUntar(t *testing.T) {
     		},
     		{
     			name:     "gakki",
    +			data:     "tmp/gakki",
    +			fileType: SymLink,
    +		},
    +		{
    +			name:     "relative_to_dest",
    +			data:     path.Join(dir2, "foo"),
    +			fileType: SymLink,
    +		},
    +		{
    +			name:     "tricky_relative",
    +			data:     path.Join(dir3, "xyz"),
    +			omitted:  true,
    +			fileType: SymLink,
    +		},
    +		{
    +			name:     "absolute_path",
     			data:     "/tmp/gakki",
    +			omitted:  true,
     			fileType: SymLink,
     		},
     		{
    @@ -268,13 +291,15 @@ func TestTarUntar(t *testing.T) {
     		}
     	}
     
    +	opts := NewCopyOptions(genericclioptions.NewTestIOStreamsDiscard())
    +
     	writer := &bytes.Buffer{}
     	if err := makeTar(dir, dir, writer); err != nil {
     		t.Fatalf("unexpected error: %v", err)
     	}
     
     	reader := bytes.NewBuffer(writer.Bytes())
    -	if err := untarAll(reader, dir2, ""); err != nil {
    +	if err := opts.untarAll(reader, dir2, ""); err != nil {
     		t.Fatalf("unexpected error: %v", err)
     	}
     
    @@ -286,7 +311,12 @@ func TestTarUntar(t *testing.T) {
     			cmpFileData(t, filePath, file.data)
     		} else if file.fileType == SymLink {
     			dest, err := os.Readlink(filePath)
    -
    +			if file.omitted {
    +				if err != nil && strings.Contains(err.Error(), "no such file or directory") {
    +					continue
    +				}
    +				t.Fatalf("expected to omit symlink for %s", filePath)
    +			}
     			if err != nil {
     				t.Fatalf("unexpected error: %v", err)
     			}
    @@ -304,6 +334,38 @@ func TestTarUntar(t *testing.T) {
     	}
     }
     
    +func TestTarUntarWrongPrefix(t *testing.T) {
    +	dir, err := ioutil.TempDir("", "input")
    +	checkErr(t, err)
    +	dir2, err := ioutil.TempDir("", "output")
    +	checkErr(t, err)
    +
    +	dir = dir + "/"
    +	defer func() {
    +		os.RemoveAll(dir)
    +		os.RemoveAll(dir2)
    +	}()
    +
    +	filepath := path.Join(dir, "foo")
    +	if err := os.MkdirAll(path.Dir(filepath), 0755); err != nil {
    +		t.Fatalf("unexpected error: %v", err)
    +	}
    +	createTmpFile(t, filepath, "sample data")
    +
    +	opts := NewCopyOptions(genericclioptions.NewTestIOStreamsDiscard())
    +
    +	writer := &bytes.Buffer{}
    +	if err := makeTar(dir, dir, writer); err != nil {
    +		t.Fatalf("unexpected error: %v", err)
    +	}
    +
    +	reader := bytes.NewBuffer(writer.Bytes())
    +	err = opts.untarAll(reader, dir2, "verylongprefix-showing-the-tar-was-tempered-with")
    +	if err == nil || !strings.Contains(err.Error(), "tar contents corrupted") {
    +		t.Fatalf("unexpected error: %v", err)
    +	}
    +}
    +
     // TestCopyToLocalFileOrDir tests untarAll in two cases :
     // 1: copy pod file to local file
     // 2: copy pod file into local directory
    @@ -376,7 +438,8 @@ func TestCopyToLocalFileOrDir(t *testing.T) {
     			}
     			defer srcTarFile.Close()
     
    -			if err := untarAll(srcTarFile, destPath, getPrefix(srcFilePath)); err != nil {
    +			opts := NewCopyOptions(genericclioptions.NewTestIOStreamsDiscard())
    +			if err := opts.untarAll(srcTarFile, destPath, getPrefix(srcFilePath)); err != nil {
     				t.Errorf("unexpected error: %v", err)
     				t.FailNow()
     			}
    @@ -503,7 +566,8 @@ func TestBadTar(t *testing.T) {
     		t.FailNow()
     	}
     
    -	if err := untarAll(&buf, dir, "/prefix"); err != nil {
    +	opts := NewCopyOptions(genericclioptions.NewTestIOStreamsDiscard())
    +	if err := opts.untarAll(&buf, dir, "/prefix"); err != nil {
     		t.Errorf("unexpected error: %v ", err)
     		t.FailNow()
     	}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

16

News mentions

0

No linked articles in our index yet.