Skip to content

Commit 786acdb

Browse files
committed
Remove symlink support from kubectl cp
1 parent c7c89f8 commit 786acdb

File tree

3 files changed

+46
-68
lines changed

3 files changed

+46
-68
lines changed

hack/.staticcheck_failures

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ pkg/credentialprovider
2626
pkg/credentialprovider/aws
2727
pkg/credentialprovider/azure
2828
pkg/kubeapiserver/admission
29-
pkg/kubectl/cmd/cp
3029
pkg/kubectl/cmd/get
3130
pkg/kubelet/apis/podresources
3231
pkg/kubelet/cm/devicemanager

pkg/kubectl/cmd/cp/cp.go

Lines changed: 34 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ var (
4545
# !!!Important Note!!!
4646
# Requires that the 'tar' binary is present in your container
4747
# image. If 'tar' is not present, 'kubectl cp' will fail.
48+
#
49+
# For advanced use cases, such as symlinks, wildcard expansion or
50+
# file mode preservation consider using 'kubectl exec'.
51+
52+
# Copy /tmp/foo local file to /tmp/bar in a remote pod in namespace <some-namespace>
53+
tar cf - /tmp/foo | kubectl exec -i -n <some-namespace> <some-pod> -- tar xf - -C /tmp/bar
54+
55+
# Copy /tmp/foo from a remote pod to /tmp/bar locally
56+
kubectl exec -n <some-namespace> <some-pod> -- tar cf - /tmp/foo | tar xf - -C /tmp/bar
4857
4958
# Copy /tmp/foo_dir local directory to /tmp/bar_dir in a remote pod in the default namespace
5059
kubectl cp /tmp/foo_dir <some-pod>:/tmp/bar_dir
@@ -71,8 +80,9 @@ type CopyOptions struct {
7180
Namespace string
7281
NoPreserve bool
7382

74-
ClientConfig *restclient.Config
75-
Clientset kubernetes.Interface
83+
ClientConfig *restclient.Config
84+
Clientset kubernetes.Interface
85+
ExecParentCmdName string
7686

7787
genericclioptions.IOStreams
7888
}
@@ -143,6 +153,10 @@ func extractFileSpec(arg string) (fileSpec, error) {
143153

144154
// Complete completes all the required options
145155
func (o *CopyOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
156+
if cmd.Parent() != nil {
157+
o.ExecParentCmdName = cmd.Parent().CommandPath()
158+
}
159+
146160
var err error
147161
o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
148162
if err != nil {
@@ -307,7 +321,7 @@ func (o *CopyOptions) copyFromPod(src, dest fileSpec) error {
307321
// remove extraneous path shortcuts - these could occur if a path contained extra "../"
308322
// and attempted to navigate beyond "/" in a remote filesystem
309323
prefix = stripPathShortcuts(prefix)
310-
return o.untarAll(reader, dest.File, prefix)
324+
return o.untarAll(src, reader, dest.File, prefix)
311325
}
312326

313327
// stripPathShortcuts removes any leading or trailing "../" from a given path
@@ -412,7 +426,8 @@ func recursiveTar(srcBase, srcFile, destBase, destFile string, tw *tar.Writer) e
412426
return nil
413427
}
414428

415-
func (o *CopyOptions) untarAll(reader io.Reader, destDir, prefix string) error {
429+
func (o *CopyOptions) untarAll(src fileSpec, reader io.Reader, destDir, prefix string) error {
430+
symlinkWarningPrinted := false
416431
// TODO: use compression here?
417432
tarReader := tar.NewReader(reader)
418433
for {
@@ -453,48 +468,25 @@ func (o *CopyOptions) untarAll(reader io.Reader, destDir, prefix string) error {
453468
continue
454469
}
455470

456-
// We need to ensure that the destination file is always within boundries
457-
// of the destination directory. This prevents any kind of path traversal
458-
// from within tar archive.
459-
evaledPath, err := filepath.EvalSymlinks(baseName)
471+
if mode&os.ModeSymlink != 0 {
472+
if !symlinkWarningPrinted && len(o.ExecParentCmdName) > 0 {
473+
fmt.Fprintf(o.IOStreams.ErrOut, "warning: file %q is a symlink, skipping (consider using \"%s exec -n %q %q -- tar cf - %q | tar xf -\")\n", destFileName, o.ExecParentCmdName, src.PodNamespace, src.PodName, src.File)
474+
symlinkWarningPrinted = true
475+
continue
476+
}
477+
fmt.Fprintf(o.IOStreams.ErrOut, "warning: skipping symlink: %q -> %q\n", destFileName, header.Linkname)
478+
continue
479+
}
480+
outFile, err := os.Create(destFileName)
460481
if err != nil {
461482
return err
462483
}
463-
// For scrutiny we verify both the actual destination as well as we follow
464-
// all the links that might lead outside of the destination directory.
465-
if !isDestRelative(destDir, filepath.Join(evaledPath, filepath.Base(destFileName))) {
466-
fmt.Fprintf(o.IOStreams.ErrOut, "warning: file %q is outside target destination, skipping\n", destFileName)
467-
continue
484+
defer outFile.Close()
485+
if _, err := io.Copy(outFile, tarReader); err != nil {
486+
return err
468487
}
469-
470-
if mode&os.ModeSymlink != 0 {
471-
linkname := header.Linkname
472-
// We need to ensure that the link destination is always within boundries
473-
// of the destination directory. This prevents any kind of path traversal
474-
// from within tar archive.
475-
linkTarget := linkname
476-
if !filepath.IsAbs(linkname) {
477-
linkTarget = filepath.Join(evaledPath, linkname)
478-
}
479-
if !isDestRelative(destDir, linkTarget) {
480-
fmt.Fprintf(o.IOStreams.ErrOut, "warning: link %q is pointing to %q which is outside target destination, skipping\n", destFileName, header.Linkname)
481-
continue
482-
}
483-
if err := os.Symlink(linkname, destFileName); err != nil {
484-
return err
485-
}
486-
} else {
487-
outFile, err := os.Create(destFileName)
488-
if err != nil {
489-
return err
490-
}
491-
defer outFile.Close()
492-
if _, err := io.Copy(outFile, tarReader); err != nil {
493-
return err
494-
}
495-
if err := outFile.Close(); err != nil {
496-
return err
497-
}
488+
if err := outFile.Close(); err != nil {
489+
return err
498490
}
499491
}
500492

pkg/kubectl/cmd/cp/cp_test.go

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -302,11 +302,13 @@ func TestTarUntar(t *testing.T) {
302302
{
303303
name: "gakki",
304304
data: "tmp/gakki",
305+
omitted: true,
305306
fileType: SymLink,
306307
},
307308
{
308309
name: "relative_to_dest",
309310
data: path.Join(dir2, "foo"),
311+
omitted: true,
310312
fileType: SymLink,
311313
},
312314
{
@@ -358,7 +360,7 @@ func TestTarUntar(t *testing.T) {
358360
}
359361

360362
reader := bytes.NewBuffer(writer.Bytes())
361-
if err := opts.untarAll(reader, dir2, ""); err != nil {
363+
if err := opts.untarAll(fileSpec{}, reader, dir2, ""); err != nil {
362364
t.Fatalf("unexpected error: %v", err)
363365
}
364366

@@ -419,7 +421,7 @@ func TestTarUntarWrongPrefix(t *testing.T) {
419421
}
420422

421423
reader := bytes.NewBuffer(writer.Bytes())
422-
err = opts.untarAll(reader, dir2, "verylongprefix-showing-the-tar-was-tempered-with")
424+
err = opts.untarAll(fileSpec{}, reader, dir2, "verylongprefix-showing-the-tar-was-tempered-with")
423425
if err == nil || !strings.Contains(err.Error(), "tar contents corrupted") {
424426
t.Fatalf("unexpected error: %v", err)
425427
}
@@ -534,7 +536,7 @@ func TestBadTar(t *testing.T) {
534536
}
535537

536538
opts := NewCopyOptions(genericclioptions.NewTestIOStreamsDiscard())
537-
if err := opts.untarAll(&buf, dir, "/prefix"); err != nil {
539+
if err := opts.untarAll(fileSpec{}, &buf, dir, "/prefix"); err != nil {
538540
t.Errorf("unexpected error: %v ", err)
539541
t.FailNow()
540542
}
@@ -780,35 +782,20 @@ func TestUntar(t *testing.T) {
780782
expected: "",
781783
}}
782784

783-
mkExpectation := func(expected, suffix string) string {
784-
if expected == "" {
785-
return ""
786-
}
787-
return expected + suffix
788-
}
789-
mkBacklinkExpectation := func(expected, suffix string) string {
790-
// "resolve" the back link relative to the expectation
791-
targetDir := filepath.Dir(filepath.Dir(expected))
792-
// If the "resolved" target is not nested in basedir, it is escaping.
793-
if !filepath.HasPrefix(targetDir, basedir) {
794-
return ""
795-
}
796-
return expected + suffix
797-
}
798785
links := []file{}
799786
for _, f := range files {
800787
links = append(links, file{
801788
path: f.path + "-innerlink",
802789
linkTarget: "link-target",
803-
expected: mkExpectation(f.expected, "-innerlink"),
790+
expected: "",
804791
}, file{
805792
path: f.path + "-innerlink-abs",
806793
linkTarget: filepath.Join(basedir, "link-target"),
807-
expected: mkExpectation(f.expected, "-innerlink-abs"),
794+
expected: "",
808795
}, file{
809796
path: f.path + "-backlink",
810797
linkTarget: filepath.Join("..", "link-target"),
811-
expected: mkBacklinkExpectation(f.expected, "-backlink"),
798+
expected: "",
812799
}, file{
813800
path: f.path + "-outerlink-abs",
814801
linkTarget: filepath.Join(testdir, "link-target"),
@@ -832,7 +819,7 @@ func TestUntar(t *testing.T) {
832819
file{
833820
path: "nested/again/back-link",
834821
linkTarget: "../../nested",
835-
expected: filepath.Join(basedir, "nested/again/back-link"),
822+
expected: "",
836823
},
837824
file{
838825
path: "nested/again/back-link/../../../back-link-file",
@@ -844,7 +831,7 @@ func TestUntar(t *testing.T) {
844831
file{
845832
path: "nested/back-link-first",
846833
linkTarget: "../",
847-
expected: filepath.Join(basedir, "nested/back-link-first"),
834+
expected: "",
848835
},
849836
file{
850837
path: "nested/back-link-first/back-link-second",
@@ -892,7 +879,7 @@ func TestUntar(t *testing.T) {
892879
output := (*testWriter)(t)
893880
opts := NewCopyOptions(genericclioptions.IOStreams{In: &bytes.Buffer{}, Out: output, ErrOut: output})
894881

895-
require.NoError(t, opts.untarAll(buf, filepath.Join(basedir), ""))
882+
require.NoError(t, opts.untarAll(fileSpec{}, buf, filepath.Join(basedir), ""))
896883

897884
filepath.Walk(testdir, func(path string, info os.FileInfo, err error) error {
898885
if err != nil {
@@ -943,7 +930,7 @@ func TestUntar_SingleFile(t *testing.T) {
943930
output := (*testWriter)(t)
944931
opts := NewCopyOptions(genericclioptions.IOStreams{In: &bytes.Buffer{}, Out: output, ErrOut: output})
945932

946-
require.NoError(t, opts.untarAll(buf, filepath.Join(dest), srcName))
933+
require.NoError(t, opts.untarAll(fileSpec{}, buf, filepath.Join(dest), srcName))
947934
cmpFileData(t, dest, content)
948935
}
949936

0 commit comments

Comments
 (0)