Skip to content

Commit 29bf16c

Browse files
authored
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
1 parent fb91123 commit 29bf16c

File tree

2 files changed

+117
-35
lines changed

2 files changed

+117
-35
lines changed

api/internal/loader/fileloader.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,26 @@ func newLoaderAtConfirmedDir(
147147
}
148148
}
149149

150+
// hasAmbiguousPathSegment reports whether path contains a segment with ".."
151+
// embedded in it (not ".." itself). This pattern results from YAML indentation
152+
// errors where two list entries merge into one scalar, e.g.:
153+
//
154+
// resources:
155+
// - ../../base
156+
// - ../../shared/prod
157+
//
158+
// YAML parses this as "../../base - ../../shared/prod", producing segment
159+
// "base - .." which filepath.Clean absorbs, silently resolving to an
160+
// unintended directory.
161+
func hasAmbiguousPathSegment(path string) bool {
162+
for _, part := range strings.Split(filepath.ToSlash(path), "/") {
163+
if part != "" && part != "." && part != ".." && strings.Contains(part, "..") {
164+
return true
165+
}
166+
}
167+
return false
168+
}
169+
150170
// New returns a new Loader, rooted relative to current loader,
151171
// or rooted in a temp directory holding a git repo clone.
152172
func (fl *FileLoader) New(path string) (ifc.Loader, error) {
@@ -167,6 +187,13 @@ func (fl *FileLoader) New(path string) (ifc.Loader, error) {
167187
if filepath.IsAbs(path) {
168188
return nil, fmt.Errorf("new root '%s' cannot be absolute", path)
169189
}
190+
if hasAmbiguousPathSegment(path) {
191+
return nil, fmt.Errorf(
192+
"path %q is ambiguous: resolves to %q after normalization, "+
193+
"which is likely not the intended target; check for YAML "+
194+
"indentation errors in your kustomization file",
195+
path, filepath.Clean(path))
196+
}
170197
root, err := filesys.ConfirmDir(fl.fSys, fl.root.Join(path))
171198
if err != nil {
172199
return nil, errors.WrapPrefixf(err, "%s", ErrRtNotDir.Error())

api/internal/loader/fileloader_test.go

Lines changed: 90 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ type testData struct {
6666
expectedContent string
6767
}
6868

69-
var testCases = []testData{
69+
var testCases = []testData{ //nolint:gochecknoglobals
7070
{
7171
path: "foo/project/fileA.yaml",
7272
expectedContent: "fileA content",
@@ -88,7 +88,7 @@ var testCases = []testData{
8888
func MakeFakeFs(td []testData) filesys.FileSystem {
8989
fSys := filesys.MakeFsInMemory()
9090
for _, x := range td {
91-
fSys.WriteFile(x.path, []byte(x.expectedContent))
91+
_ = fSys.WriteFile(x.path, []byte(x.expectedContent))
9292
}
9393
return fSys
9494
}
@@ -161,31 +161,31 @@ func TestLoaderBadRelative(t *testing.T) {
161161
require.Equal("/foo/project/subdir1", l1.Root())
162162

163163
// Cannot cd into a file.
164-
l2, err := l1.New("fileB.yaml")
164+
_, err = l1.New("fileB.yaml")
165165
require.Error(err)
166166

167167
// It's not okay to stay at the same place.
168-
l2, err = l1.New(filesys.SelfDir)
168+
_, err = l1.New(filesys.SelfDir)
169169
require.Error(err)
170170

171171
// It's not okay to go up and back down into same place.
172-
l2, err = l1.New("../subdir1")
172+
_, err = l1.New("../subdir1")
173173
require.Error(err)
174174

175175
// It's not okay to go up via a relative path.
176-
l2, err = l1.New("..")
176+
_, err = l1.New("..")
177177
require.Error(err)
178178

179179
// It's not okay to go up via an absolute path.
180-
l2, err = l1.New("/foo/project")
180+
_, err = l1.New("/foo/project")
181181
require.Error(err)
182182

183183
// It's not okay to go to the root.
184-
l2, err = l1.New("/")
184+
_, err = l1.New("/")
185185
require.Error(err)
186186

187187
// It's okay to go up and down to a sibling.
188-
l2, err = l1.New("../subdir2")
188+
l2, err := l1.New("../subdir2")
189189
require.NoError(err)
190190
require.Equal("/foo/project/subdir2", l2.Root())
191191

@@ -198,7 +198,7 @@ func TestLoaderBadRelative(t *testing.T) {
198198

199199
// It's not OK to go over to a previously visited directory.
200200
// Must disallow going back and forth in a cycle.
201-
l1, err = l2.New("../subdir1")
201+
_, err = l2.New("../subdir1")
202202
require.Error(err)
203203
}
204204

@@ -207,6 +207,60 @@ func TestNewEmptyLoader(t *testing.T) {
207207
require.Error(t, err)
208208
}
209209

210+
func TestLoaderRejectsMalformedPath(t *testing.T) {
211+
// A YAML indentation error can collapse two resource entries into one:
212+
// resources:
213+
// - ../../base
214+
// - ../../shared/prod
215+
// becomes the single string: "../../base - ../../shared/prod"
216+
//
217+
// filepath.Clean normalizes this to "../../shared/prod", silently
218+
// dropping the "../../base" reference. The loader must reject paths
219+
// with inner ".." components that cause this silent absorption.
220+
// See https://github.com/kubernetes-sigs/kustomize/issues/5979
221+
fSys := filesys.MakeFsInMemory()
222+
require.NoError(t, fSys.MkdirAll("/base"))
223+
require.NoError(t, fSys.MkdirAll("/shared/prod"))
224+
require.NoError(t, fSys.MkdirAll("/overlays/prod1"))
225+
226+
l1 := NewLoaderOrDie(RestrictionNone, fSys, "/overlays/prod1")
227+
228+
// The exact bug from issue #5979.
229+
_, err := l1.New("../../base - ../../shared/prod")
230+
require.Error(t, err)
231+
require.Contains(t, err.Error(), "ambiguous")
232+
}
233+
234+
func TestHasAmbiguousPathSegment(t *testing.T) {
235+
cases := map[string]bool{
236+
// Safe: ".." only as standalone segments
237+
"../base": false,
238+
"../../shared/prod": false,
239+
"..": false,
240+
"foo/bar": false,
241+
"foo/bar/": false,
242+
"foo//bar": false,
243+
"./foo/bar": false,
244+
"https://root": false,
245+
"foo/../bar": false,
246+
"a/b/../../c": false,
247+
"a/..": false,
248+
// Winding paths are legitimate (used by localizer)
249+
"delta/../../../../a/b/../../alpha/beta/sibling": false,
250+
// Dangerous: segment contains embedded ".." from YAML merge errors
251+
"../../base - ../../shared/prod": true,
252+
"../../base ../../shared/prod": true,
253+
"foo/bar..baz/qux": true,
254+
"a/b/c..d/e": true,
255+
"../../base\t- ../../shared/prod": true,
256+
}
257+
for path, want := range cases {
258+
t.Run(path, func(t *testing.T) {
259+
require.Equal(t, want, hasAmbiguousPathSegment(path), "hasAmbiguousPathSegment(%q)", path)
260+
})
261+
}
262+
}
263+
210264
func TestNewRemoteLoaderDoesNotExist(t *testing.T) {
211265
_, err := makeLoader().New("https://example.com/org/repo")
212266
require.ErrorContains(t, err, "fetch")
@@ -265,23 +319,24 @@ const (
265319
// └── exteriorData
266320
func commonSetupForLoaderRestrictionTest(t *testing.T) (string, filesys.FileSystem) {
267321
t.Helper()
322+
require := require.New(t)
268323
fSys, tmpDir := setupOnDisk(t)
269324
dir := tmpDir.String()
270325

271-
fSys.Mkdir(filepath.Join(dir, "base"))
326+
require.NoError(fSys.Mkdir(filepath.Join(dir, "base")))
272327

273-
fSys.WriteFile(
274-
filepath.Join(dir, "base", "okayData"), []byte(contentOk))
328+
require.NoError(fSys.WriteFile(
329+
filepath.Join(dir, "base", "okayData"), []byte(contentOk)))
275330

276-
fSys.WriteFile(
277-
filepath.Join(dir, "exteriorData"), []byte(contentExteriorData))
331+
require.NoError(fSys.WriteFile(
332+
filepath.Join(dir, "exteriorData"), []byte(contentExteriorData)))
278333

279-
os.Symlink(
334+
require.NoError(os.Symlink(
280335
filepath.Join(dir, "base", "okayData"),
281-
filepath.Join(dir, "base", "symLinkToOkayData"))
282-
os.Symlink(
336+
filepath.Join(dir, "base", "symLinkToOkayData")))
337+
require.NoError(os.Symlink(
283338
filepath.Join(dir, "exteriorData"),
284-
filepath.Join(dir, "base", "symLinkToExteriorData"))
339+
filepath.Join(dir, "base", "symLinkToExteriorData")))
285340
return dir, fSys
286341
}
287342

@@ -391,14 +446,14 @@ func TestNewLoaderAtGitClone(t *testing.T) {
391446
url := rootURL + "/" + pathInRepo
392447
coRoot := "/tmp"
393448
fSys := filesys.MakeFsInMemory()
394-
fSys.MkdirAll(coRoot)
395-
fSys.MkdirAll(coRoot + "/" + pathInRepo)
396-
fSys.WriteFile(
449+
require.NoError(fSys.MkdirAll(coRoot))
450+
require.NoError(fSys.MkdirAll(coRoot + "/" + pathInRepo))
451+
require.NoError(fSys.WriteFile(
397452
coRoot+"/"+pathInRepo+"/"+
398453
konfig.DefaultKustomizationFileName(),
399454
[]byte(`
400455
whatever
401-
`))
456+
`)))
402457

403458
repoSpec, err := git.NewRepoSpecFromURL(url)
404459
require.NoError(err)
@@ -418,7 +473,7 @@ whatever
418473
require.Error(err)
419474

420475
pathInRepo = "foo/overlay"
421-
fSys.MkdirAll(coRoot + "/" + pathInRepo)
476+
require.NoError(fSys.MkdirAll(coRoot + "/" + pathInRepo))
422477
url = rootURL + "/" + pathInRepo
423478
l2, err := l.New(url)
424479
require.NoError(err)
@@ -435,9 +490,9 @@ func TestLoaderDisallowsLocalBaseFromRemoteOverlay(t *testing.T) {
435490
topDir := "/whatever"
436491
cloneRoot := topDir + "/someClone"
437492
fSys := filesys.MakeFsInMemory()
438-
fSys.MkdirAll(topDir + "/highBase")
439-
fSys.MkdirAll(cloneRoot + "/foo/base")
440-
fSys.MkdirAll(cloneRoot + "/foo/overlay")
493+
require.NoError(fSys.MkdirAll(topDir + "/highBase"))
494+
require.NoError(fSys.MkdirAll(cloneRoot + "/foo/base"))
495+
require.NoError(fSys.MkdirAll(cloneRoot + "/foo/overlay"))
441496

442497
var l1 ifc.Loader
443498

@@ -448,7 +503,7 @@ func TestLoaderDisallowsLocalBaseFromRemoteOverlay(t *testing.T) {
448503
require.Equal(cloneRoot+"/foo/overlay", l1.Root())
449504

450505
l2, err := l1.New("../base")
451-
require.NoError(nil)
506+
require.NoError(err)
452507
require.Equal(cloneRoot+"/foo/base", l2.Root())
453508

454509
l3, err := l2.New("../../../highBase")
@@ -513,8 +568,8 @@ func TestLocalLoaderReferencingGitBase(t *testing.T) {
513568
topDir := "/whatever"
514569
cloneRoot := topDir + "/someClone"
515570
fSys := filesys.MakeFsInMemory()
516-
fSys.MkdirAll(topDir)
517-
fSys.MkdirAll(cloneRoot + "/foo/base")
571+
require.NoError(fSys.MkdirAll(topDir))
572+
require.NoError(fSys.MkdirAll(cloneRoot + "/foo/base"))
518573

519574
l1 := newLoaderAtConfirmedDir(
520575
RestrictionRootOnly, filesys.ConfirmedDir(topDir), fSys, nil,
@@ -534,8 +589,8 @@ func TestRepoDirectCycleDetection(t *testing.T) {
534589
topDir := "/cycles"
535590
cloneRoot := topDir + "/someClone"
536591
fSys := filesys.MakeFsInMemory()
537-
fSys.MkdirAll(topDir)
538-
fSys.MkdirAll(cloneRoot)
592+
require.NoError(fSys.MkdirAll(topDir))
593+
require.NoError(fSys.MkdirAll(cloneRoot))
539594

540595
l1 := newLoaderAtConfirmedDir(
541596
RestrictionRootOnly, filesys.ConfirmedDir(topDir), fSys, nil,
@@ -556,8 +611,8 @@ func TestRepoIndirectCycleDetection(t *testing.T) {
556611
topDir := "/cycles"
557612
cloneRoot := topDir + "/someClone"
558613
fSys := filesys.MakeFsInMemory()
559-
fSys.MkdirAll(topDir)
560-
fSys.MkdirAll(cloneRoot)
614+
require.NoError(fSys.MkdirAll(topDir))
615+
require.NoError(fSys.MkdirAll(cloneRoot))
561616

562617
l0 := newLoaderAtConfirmedDir(
563618
RestrictionRootOnly, filesys.ConfirmedDir(topDir), fSys, nil,
@@ -629,7 +684,7 @@ func TestLoaderHTTP(t *testing.T) {
629684
u := req.URL.String()
630685
require.Equal(x.path, u)
631686
return &http.Response{
632-
StatusCode: 200,
687+
StatusCode: http.StatusOK,
633688
Body: io.NopCloser(bytes.NewBufferString(x.expectedContent)),
634689
Header: make(http.Header),
635690
}

0 commit comments

Comments
 (0)