From bd2ca6aa74a7d3ac2063e4aa7c7dfecc22ee84c7 Mon Sep 17 00:00:00 2001 From: Marc Campbell Date: Sun, 17 May 2026 09:16:34 -0700 Subject: [PATCH] prevent path traversal --- pkg/kots/release/save.go | 40 ++++++++++++++++++++++-- pkg/kots/release/save_test.go | 57 +++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/pkg/kots/release/save.go b/pkg/kots/release/save.go index 7533dc6eb..74945d403 100644 --- a/pkg/kots/release/save.go +++ b/pkg/kots/release/save.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "github.com/pkg/errors" releaseTypes "github.com/replicatedhq/replicated/pkg/kots/release/types" @@ -52,11 +53,16 @@ func writeReleaseFiles(dstDir string, specs []releaseTypes.KotsSingleSpec, log * func writeReleaseDirectory(dstDir string, spec releaseTypes.KotsSingleSpec, log *logger.Logger) error { log.ChildActionWithoutSpinner("%s", spec.Path) - if err := os.Mkdir(filepath.Join(dstDir, spec.Path), 0755); err != nil && !os.IsExist(err) { + path, err := resolveReleasePath(dstDir, spec.Path) + if err != nil { + return err + } + + if err := os.Mkdir(path, 0755); err != nil && !os.IsExist(err) { return errors.Wrap(err, "create directory") } - err := writeReleaseFiles(dstDir, spec.Children, log) + err = writeReleaseFiles(dstDir, spec.Children, log) if err != nil { return errors.Wrap(err, "write children") } @@ -82,10 +88,38 @@ func writeReleaseFile(dstDir string, spec releaseTypes.KotsSingleSpec, log *logg content = []byte(spec.Content) } - err := ioutil.WriteFile(filepath.Join(dstDir, spec.Path), content, 0644) + path, err := resolveReleasePath(dstDir, spec.Path) + if err != nil { + return err + } + + err = ioutil.WriteFile(path, content, 0644) if err != nil { return errors.Wrap(err, "write file") } return nil } + +func resolveReleasePath(dstDir string, releasePath string) (string, error) { + cleanReleasePath := filepath.Clean(releasePath) + if filepath.IsAbs(releasePath) || + cleanReleasePath == "." || + cleanReleasePath == ".." || + strings.HasPrefix(cleanReleasePath, ".."+string(filepath.Separator)) { + return "", errors.Errorf("invalid release path %q", releasePath) + } + + cleanDstDir := filepath.Clean(dstDir) + path := filepath.Join(cleanDstDir, cleanReleasePath) + + rel, err := filepath.Rel(cleanDstDir, path) + if err != nil { + return "", errors.Wrap(err, "get relative release path") + } + if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) || filepath.IsAbs(rel) { + return "", errors.Errorf("release path escapes destination %q", releasePath) + } + + return path, nil +} diff --git a/pkg/kots/release/save_test.go b/pkg/kots/release/save_test.go index 603cb64f3..e0f5d7574 100644 --- a/pkg/kots/release/save_test.go +++ b/pkg/kots/release/save_test.go @@ -2,12 +2,14 @@ package release import ( _ "embed" + "encoding/json" "io/ioutil" "os" "path/filepath" "testing" "github.com/pkg/errors" + releaseTypes "github.com/replicatedhq/replicated/pkg/kots/release/types" "github.com/replicatedhq/replicated/pkg/logger" "github.com/replicatedhq/replicated/pkg/types" "github.com/stretchr/testify/assert" @@ -108,3 +110,58 @@ func Test_Save(t *testing.T) { require.NoError(t, err) }) } + +func Test_SaveRejectsEscapingPaths(t *testing.T) { + tests := []struct { + name string + path func(string) string + escapePath func(string) string + }{ + { + name: "parent traversal", + path: func(string) string { + return "../escape.yaml" + }, + escapePath: func(dstDir string) string { + return filepath.Join(dstDir, "..", "escape.yaml") + }, + }, + { + name: "absolute path", + path: func(dstDir string) string { + return filepath.Join(filepath.Dir(dstDir), filepath.Base(dstDir)+"-absolute-escape.yaml") + }, + escapePath: func(dstDir string) string { + return filepath.Join(filepath.Dir(dstDir), filepath.Base(dstDir)+"-absolute-escape.yaml") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dstDir, err := os.MkdirTemp("", "kots-release-save-test") + require.NoError(t, err) + defer os.RemoveAll(dstDir) + + escapePath := tt.escapePath(dstDir) + _ = os.Remove(escapePath) + + config, err := json.Marshal([]releaseTypes.KotsSingleSpec{ + { + Path: tt.path(dstDir), + Content: "owned", + }, + }) + require.NoError(t, err) + + release := &types.AppRelease{ + Config: string(config), + } + err = Save(dstDir, release, logger.NewLogger(os.Stdout)) + require.Error(t, err) + + _, err = os.Stat(escapePath) + require.True(t, os.IsNotExist(err), "expected %s not to be created", escapePath) + }) + } +}