mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-05-17 18:25:26 +00:00
Reject ambiguous resource paths with inner ".." to prevent silent misresolution (#6087)
* Reject paths with inner '..' in FileLoader.New to prevent silent misresolution * Refactor hasInnerDotDot to two-phase loop eliminating mutable state * Narrow check to embedded '..' segments to allow legitimate winding paths * Fix gofmt alignment and trailing whitespace in new test functions * Fix pre-existing lint errors in fileloader_test.go
This commit is contained in:
@@ -147,6 +147,26 @@ func newLoaderAtConfirmedDir(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hasAmbiguousPathSegment reports whether path contains a segment with ".."
|
||||||
|
// embedded in it (not ".." itself). This pattern results from YAML indentation
|
||||||
|
// errors where two list entries merge into one scalar, e.g.:
|
||||||
|
//
|
||||||
|
// resources:
|
||||||
|
// - ../../base
|
||||||
|
// - ../../shared/prod
|
||||||
|
//
|
||||||
|
// YAML parses this as "../../base - ../../shared/prod", producing segment
|
||||||
|
// "base - .." which filepath.Clean absorbs, silently resolving to an
|
||||||
|
// unintended directory.
|
||||||
|
func hasAmbiguousPathSegment(path string) bool {
|
||||||
|
for _, part := range strings.Split(filepath.ToSlash(path), "/") {
|
||||||
|
if part != "" && part != "." && part != ".." && strings.Contains(part, "..") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// New returns a new Loader, rooted relative to current loader,
|
// New returns a new Loader, rooted relative to current loader,
|
||||||
// or rooted in a temp directory holding a git repo clone.
|
// or rooted in a temp directory holding a git repo clone.
|
||||||
func (fl *FileLoader) New(path string) (ifc.Loader, error) {
|
func (fl *FileLoader) New(path string) (ifc.Loader, error) {
|
||||||
@@ -167,6 +187,13 @@ func (fl *FileLoader) New(path string) (ifc.Loader, error) {
|
|||||||
if filepath.IsAbs(path) {
|
if filepath.IsAbs(path) {
|
||||||
return nil, fmt.Errorf("new root '%s' cannot be absolute", path)
|
return nil, fmt.Errorf("new root '%s' cannot be absolute", path)
|
||||||
}
|
}
|
||||||
|
if hasAmbiguousPathSegment(path) {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"path %q is ambiguous: resolves to %q after normalization, "+
|
||||||
|
"which is likely not the intended target; check for YAML "+
|
||||||
|
"indentation errors in your kustomization file",
|
||||||
|
path, filepath.Clean(path))
|
||||||
|
}
|
||||||
root, err := filesys.ConfirmDir(fl.fSys, fl.root.Join(path))
|
root, err := filesys.ConfirmDir(fl.fSys, fl.root.Join(path))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WrapPrefixf(err, "%s", ErrRtNotDir.Error())
|
return nil, errors.WrapPrefixf(err, "%s", ErrRtNotDir.Error())
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ type testData struct {
|
|||||||
expectedContent string
|
expectedContent string
|
||||||
}
|
}
|
||||||
|
|
||||||
var testCases = []testData{
|
var testCases = []testData{ //nolint:gochecknoglobals
|
||||||
{
|
{
|
||||||
path: "foo/project/fileA.yaml",
|
path: "foo/project/fileA.yaml",
|
||||||
expectedContent: "fileA content",
|
expectedContent: "fileA content",
|
||||||
@@ -88,7 +88,7 @@ var testCases = []testData{
|
|||||||
func MakeFakeFs(td []testData) filesys.FileSystem {
|
func MakeFakeFs(td []testData) filesys.FileSystem {
|
||||||
fSys := filesys.MakeFsInMemory()
|
fSys := filesys.MakeFsInMemory()
|
||||||
for _, x := range td {
|
for _, x := range td {
|
||||||
fSys.WriteFile(x.path, []byte(x.expectedContent))
|
_ = fSys.WriteFile(x.path, []byte(x.expectedContent))
|
||||||
}
|
}
|
||||||
return fSys
|
return fSys
|
||||||
}
|
}
|
||||||
@@ -161,31 +161,31 @@ func TestLoaderBadRelative(t *testing.T) {
|
|||||||
require.Equal("/foo/project/subdir1", l1.Root())
|
require.Equal("/foo/project/subdir1", l1.Root())
|
||||||
|
|
||||||
// Cannot cd into a file.
|
// Cannot cd into a file.
|
||||||
l2, err := l1.New("fileB.yaml")
|
_, err = l1.New("fileB.yaml")
|
||||||
require.Error(err)
|
require.Error(err)
|
||||||
|
|
||||||
// It's not okay to stay at the same place.
|
// It's not okay to stay at the same place.
|
||||||
l2, err = l1.New(filesys.SelfDir)
|
_, err = l1.New(filesys.SelfDir)
|
||||||
require.Error(err)
|
require.Error(err)
|
||||||
|
|
||||||
// It's not okay to go up and back down into same place.
|
// It's not okay to go up and back down into same place.
|
||||||
l2, err = l1.New("../subdir1")
|
_, err = l1.New("../subdir1")
|
||||||
require.Error(err)
|
require.Error(err)
|
||||||
|
|
||||||
// It's not okay to go up via a relative path.
|
// It's not okay to go up via a relative path.
|
||||||
l2, err = l1.New("..")
|
_, err = l1.New("..")
|
||||||
require.Error(err)
|
require.Error(err)
|
||||||
|
|
||||||
// It's not okay to go up via an absolute path.
|
// It's not okay to go up via an absolute path.
|
||||||
l2, err = l1.New("/foo/project")
|
_, err = l1.New("/foo/project")
|
||||||
require.Error(err)
|
require.Error(err)
|
||||||
|
|
||||||
// It's not okay to go to the root.
|
// It's not okay to go to the root.
|
||||||
l2, err = l1.New("/")
|
_, err = l1.New("/")
|
||||||
require.Error(err)
|
require.Error(err)
|
||||||
|
|
||||||
// It's okay to go up and down to a sibling.
|
// It's okay to go up and down to a sibling.
|
||||||
l2, err = l1.New("../subdir2")
|
l2, err := l1.New("../subdir2")
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
require.Equal("/foo/project/subdir2", l2.Root())
|
require.Equal("/foo/project/subdir2", l2.Root())
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ func TestLoaderBadRelative(t *testing.T) {
|
|||||||
|
|
||||||
// It's not OK to go over to a previously visited directory.
|
// It's not OK to go over to a previously visited directory.
|
||||||
// Must disallow going back and forth in a cycle.
|
// Must disallow going back and forth in a cycle.
|
||||||
l1, err = l2.New("../subdir1")
|
_, err = l2.New("../subdir1")
|
||||||
require.Error(err)
|
require.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,6 +207,60 @@ func TestNewEmptyLoader(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoaderRejectsMalformedPath(t *testing.T) {
|
||||||
|
// A YAML indentation error can collapse two resource entries into one:
|
||||||
|
// resources:
|
||||||
|
// - ../../base
|
||||||
|
// - ../../shared/prod
|
||||||
|
// becomes the single string: "../../base - ../../shared/prod"
|
||||||
|
//
|
||||||
|
// filepath.Clean normalizes this to "../../shared/prod", silently
|
||||||
|
// dropping the "../../base" reference. The loader must reject paths
|
||||||
|
// with inner ".." components that cause this silent absorption.
|
||||||
|
// See https://github.com/kubernetes-sigs/kustomize/issues/5979
|
||||||
|
fSys := filesys.MakeFsInMemory()
|
||||||
|
require.NoError(t, fSys.MkdirAll("/base"))
|
||||||
|
require.NoError(t, fSys.MkdirAll("/shared/prod"))
|
||||||
|
require.NoError(t, fSys.MkdirAll("/overlays/prod1"))
|
||||||
|
|
||||||
|
l1 := NewLoaderOrDie(RestrictionNone, fSys, "/overlays/prod1")
|
||||||
|
|
||||||
|
// The exact bug from issue #5979.
|
||||||
|
_, err := l1.New("../../base - ../../shared/prod")
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "ambiguous")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHasAmbiguousPathSegment(t *testing.T) {
|
||||||
|
cases := map[string]bool{
|
||||||
|
// Safe: ".." only as standalone segments
|
||||||
|
"../base": false,
|
||||||
|
"../../shared/prod": false,
|
||||||
|
"..": false,
|
||||||
|
"foo/bar": false,
|
||||||
|
"foo/bar/": false,
|
||||||
|
"foo//bar": false,
|
||||||
|
"./foo/bar": false,
|
||||||
|
"https://root": false,
|
||||||
|
"foo/../bar": false,
|
||||||
|
"a/b/../../c": false,
|
||||||
|
"a/..": false,
|
||||||
|
// Winding paths are legitimate (used by localizer)
|
||||||
|
"delta/../../../../a/b/../../alpha/beta/sibling": false,
|
||||||
|
// Dangerous: segment contains embedded ".." from YAML merge errors
|
||||||
|
"../../base - ../../shared/prod": true,
|
||||||
|
"../../base ../../shared/prod": true,
|
||||||
|
"foo/bar..baz/qux": true,
|
||||||
|
"a/b/c..d/e": true,
|
||||||
|
"../../base\t- ../../shared/prod": true,
|
||||||
|
}
|
||||||
|
for path, want := range cases {
|
||||||
|
t.Run(path, func(t *testing.T) {
|
||||||
|
require.Equal(t, want, hasAmbiguousPathSegment(path), "hasAmbiguousPathSegment(%q)", path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewRemoteLoaderDoesNotExist(t *testing.T) {
|
func TestNewRemoteLoaderDoesNotExist(t *testing.T) {
|
||||||
_, err := makeLoader().New("https://example.com/org/repo")
|
_, err := makeLoader().New("https://example.com/org/repo")
|
||||||
require.ErrorContains(t, err, "fetch")
|
require.ErrorContains(t, err, "fetch")
|
||||||
@@ -265,23 +319,24 @@ const (
|
|||||||
// └── exteriorData
|
// └── exteriorData
|
||||||
func commonSetupForLoaderRestrictionTest(t *testing.T) (string, filesys.FileSystem) {
|
func commonSetupForLoaderRestrictionTest(t *testing.T) (string, filesys.FileSystem) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
require := require.New(t)
|
||||||
fSys, tmpDir := setupOnDisk(t)
|
fSys, tmpDir := setupOnDisk(t)
|
||||||
dir := tmpDir.String()
|
dir := tmpDir.String()
|
||||||
|
|
||||||
fSys.Mkdir(filepath.Join(dir, "base"))
|
require.NoError(fSys.Mkdir(filepath.Join(dir, "base")))
|
||||||
|
|
||||||
fSys.WriteFile(
|
require.NoError(fSys.WriteFile(
|
||||||
filepath.Join(dir, "base", "okayData"), []byte(contentOk))
|
filepath.Join(dir, "base", "okayData"), []byte(contentOk)))
|
||||||
|
|
||||||
fSys.WriteFile(
|
require.NoError(fSys.WriteFile(
|
||||||
filepath.Join(dir, "exteriorData"), []byte(contentExteriorData))
|
filepath.Join(dir, "exteriorData"), []byte(contentExteriorData)))
|
||||||
|
|
||||||
os.Symlink(
|
require.NoError(os.Symlink(
|
||||||
filepath.Join(dir, "base", "okayData"),
|
filepath.Join(dir, "base", "okayData"),
|
||||||
filepath.Join(dir, "base", "symLinkToOkayData"))
|
filepath.Join(dir, "base", "symLinkToOkayData")))
|
||||||
os.Symlink(
|
require.NoError(os.Symlink(
|
||||||
filepath.Join(dir, "exteriorData"),
|
filepath.Join(dir, "exteriorData"),
|
||||||
filepath.Join(dir, "base", "symLinkToExteriorData"))
|
filepath.Join(dir, "base", "symLinkToExteriorData")))
|
||||||
return dir, fSys
|
return dir, fSys
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,14 +446,14 @@ func TestNewLoaderAtGitClone(t *testing.T) {
|
|||||||
url := rootURL + "/" + pathInRepo
|
url := rootURL + "/" + pathInRepo
|
||||||
coRoot := "/tmp"
|
coRoot := "/tmp"
|
||||||
fSys := filesys.MakeFsInMemory()
|
fSys := filesys.MakeFsInMemory()
|
||||||
fSys.MkdirAll(coRoot)
|
require.NoError(fSys.MkdirAll(coRoot))
|
||||||
fSys.MkdirAll(coRoot + "/" + pathInRepo)
|
require.NoError(fSys.MkdirAll(coRoot + "/" + pathInRepo))
|
||||||
fSys.WriteFile(
|
require.NoError(fSys.WriteFile(
|
||||||
coRoot+"/"+pathInRepo+"/"+
|
coRoot+"/"+pathInRepo+"/"+
|
||||||
konfig.DefaultKustomizationFileName(),
|
konfig.DefaultKustomizationFileName(),
|
||||||
[]byte(`
|
[]byte(`
|
||||||
whatever
|
whatever
|
||||||
`))
|
`)))
|
||||||
|
|
||||||
repoSpec, err := git.NewRepoSpecFromURL(url)
|
repoSpec, err := git.NewRepoSpecFromURL(url)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
@@ -418,7 +473,7 @@ whatever
|
|||||||
require.Error(err)
|
require.Error(err)
|
||||||
|
|
||||||
pathInRepo = "foo/overlay"
|
pathInRepo = "foo/overlay"
|
||||||
fSys.MkdirAll(coRoot + "/" + pathInRepo)
|
require.NoError(fSys.MkdirAll(coRoot + "/" + pathInRepo))
|
||||||
url = rootURL + "/" + pathInRepo
|
url = rootURL + "/" + pathInRepo
|
||||||
l2, err := l.New(url)
|
l2, err := l.New(url)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
@@ -435,9 +490,9 @@ func TestLoaderDisallowsLocalBaseFromRemoteOverlay(t *testing.T) {
|
|||||||
topDir := "/whatever"
|
topDir := "/whatever"
|
||||||
cloneRoot := topDir + "/someClone"
|
cloneRoot := topDir + "/someClone"
|
||||||
fSys := filesys.MakeFsInMemory()
|
fSys := filesys.MakeFsInMemory()
|
||||||
fSys.MkdirAll(topDir + "/highBase")
|
require.NoError(fSys.MkdirAll(topDir + "/highBase"))
|
||||||
fSys.MkdirAll(cloneRoot + "/foo/base")
|
require.NoError(fSys.MkdirAll(cloneRoot + "/foo/base"))
|
||||||
fSys.MkdirAll(cloneRoot + "/foo/overlay")
|
require.NoError(fSys.MkdirAll(cloneRoot + "/foo/overlay"))
|
||||||
|
|
||||||
var l1 ifc.Loader
|
var l1 ifc.Loader
|
||||||
|
|
||||||
@@ -448,7 +503,7 @@ func TestLoaderDisallowsLocalBaseFromRemoteOverlay(t *testing.T) {
|
|||||||
require.Equal(cloneRoot+"/foo/overlay", l1.Root())
|
require.Equal(cloneRoot+"/foo/overlay", l1.Root())
|
||||||
|
|
||||||
l2, err := l1.New("../base")
|
l2, err := l1.New("../base")
|
||||||
require.NoError(nil)
|
require.NoError(err)
|
||||||
require.Equal(cloneRoot+"/foo/base", l2.Root())
|
require.Equal(cloneRoot+"/foo/base", l2.Root())
|
||||||
|
|
||||||
l3, err := l2.New("../../../highBase")
|
l3, err := l2.New("../../../highBase")
|
||||||
@@ -513,8 +568,8 @@ func TestLocalLoaderReferencingGitBase(t *testing.T) {
|
|||||||
topDir := "/whatever"
|
topDir := "/whatever"
|
||||||
cloneRoot := topDir + "/someClone"
|
cloneRoot := topDir + "/someClone"
|
||||||
fSys := filesys.MakeFsInMemory()
|
fSys := filesys.MakeFsInMemory()
|
||||||
fSys.MkdirAll(topDir)
|
require.NoError(fSys.MkdirAll(topDir))
|
||||||
fSys.MkdirAll(cloneRoot + "/foo/base")
|
require.NoError(fSys.MkdirAll(cloneRoot + "/foo/base"))
|
||||||
|
|
||||||
l1 := newLoaderAtConfirmedDir(
|
l1 := newLoaderAtConfirmedDir(
|
||||||
RestrictionRootOnly, filesys.ConfirmedDir(topDir), fSys, nil,
|
RestrictionRootOnly, filesys.ConfirmedDir(topDir), fSys, nil,
|
||||||
@@ -534,8 +589,8 @@ func TestRepoDirectCycleDetection(t *testing.T) {
|
|||||||
topDir := "/cycles"
|
topDir := "/cycles"
|
||||||
cloneRoot := topDir + "/someClone"
|
cloneRoot := topDir + "/someClone"
|
||||||
fSys := filesys.MakeFsInMemory()
|
fSys := filesys.MakeFsInMemory()
|
||||||
fSys.MkdirAll(topDir)
|
require.NoError(fSys.MkdirAll(topDir))
|
||||||
fSys.MkdirAll(cloneRoot)
|
require.NoError(fSys.MkdirAll(cloneRoot))
|
||||||
|
|
||||||
l1 := newLoaderAtConfirmedDir(
|
l1 := newLoaderAtConfirmedDir(
|
||||||
RestrictionRootOnly, filesys.ConfirmedDir(topDir), fSys, nil,
|
RestrictionRootOnly, filesys.ConfirmedDir(topDir), fSys, nil,
|
||||||
@@ -556,8 +611,8 @@ func TestRepoIndirectCycleDetection(t *testing.T) {
|
|||||||
topDir := "/cycles"
|
topDir := "/cycles"
|
||||||
cloneRoot := topDir + "/someClone"
|
cloneRoot := topDir + "/someClone"
|
||||||
fSys := filesys.MakeFsInMemory()
|
fSys := filesys.MakeFsInMemory()
|
||||||
fSys.MkdirAll(topDir)
|
require.NoError(fSys.MkdirAll(topDir))
|
||||||
fSys.MkdirAll(cloneRoot)
|
require.NoError(fSys.MkdirAll(cloneRoot))
|
||||||
|
|
||||||
l0 := newLoaderAtConfirmedDir(
|
l0 := newLoaderAtConfirmedDir(
|
||||||
RestrictionRootOnly, filesys.ConfirmedDir(topDir), fSys, nil,
|
RestrictionRootOnly, filesys.ConfirmedDir(topDir), fSys, nil,
|
||||||
@@ -629,7 +684,7 @@ func TestLoaderHTTP(t *testing.T) {
|
|||||||
u := req.URL.String()
|
u := req.URL.String()
|
||||||
require.Equal(x.path, u)
|
require.Equal(x.path, u)
|
||||||
return &http.Response{
|
return &http.Response{
|
||||||
StatusCode: 200,
|
StatusCode: http.StatusOK,
|
||||||
Body: io.NopCloser(bytes.NewBufferString(x.expectedContent)),
|
Body: io.NopCloser(bytes.NewBufferString(x.expectedContent)),
|
||||||
Header: make(http.Header),
|
Header: make(http.Header),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user