kubectl cp path traversal
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.
| Package | Affected versions | Patched versions |
|---|---|---|
k8s.io/kubernetesGo | < 1.11.9 | 1.11.9 |
k8s.io/kubernetesGo | >= 1.12.0, < 1.12.7 | 1.12.7 |
k8s.io/kubernetesGo | >= 1.13.0, < 1.13.5 | 1.13.5 |
Affected products
1- Range: 1.1-1.10
Patches
147063891dd78Merge pull request #75037 from soltysh/cp_bug
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- access.redhat.com/errata/RHBA-2019:0619ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHBA-2019:0620ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHBA-2019:0636ghsavendor-advisoryx_refsource_REDHATWEB
- github.com/advisories/GHSA-34jx-wx69-9x8vghsaADVISORY
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/BPV2RE5RMOGUVP5WJMXKQJZUBBLAFZPZ/mitrevendor-advisoryx_refsource_FEDORA
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/QZB7E3DOZ5WDG46XAIU6K32CXHXPXB2F/mitrevendor-advisoryx_refsource_FEDORA
- nvd.nist.gov/vuln/detail/CVE-2019-1002101ghsaADVISORY
- www.openwall.com/lists/oss-security/2019/06/21/1ghsamailing-listx_refsource_MLISTWEB
- www.openwall.com/lists/oss-security/2019/08/05/5ghsamailing-listx_refsource_MLISTWEB
- www.securityfocus.com/bid/107652ghsavdb-entryx_refsource_BIDWEB
- github.com/kubernetes/kubernetes/commit/47063891dd782835170f500a83f37cc98c3c1013ghsaWEB
- github.com/kubernetes/kubernetes/pull/75037ghsax_refsource_MISCWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/BPV2RE5RMOGUVP5WJMXKQJZUBBLAFZPZghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/QZB7E3DOZ5WDG46XAIU6K32CXHXPXB2FghsaWEB
- www.twistlock.com/labs-blog/disclosing-directory-traversal-vulnerability-kubernetes-copy-cve-2019-1002101ghsaWEB
- www.twistlock.com/labs-blog/disclosing-directory-traversal-vulnerability-kubernetes-copy-cve-2019-1002101/mitrex_refsource_MISC
News mentions
0No linked articles in our index yet.