Skip to content

Malformed resources YAML entry silently resolves to wrong directory (../../shared/prod) instead of erroring #5979

@michaelhenkel

Description

@michaelhenkel

Summary

If a kustomization.yaml contains a malformed resources list (extra indent in front of a dash), YAML parsing collapses two list items into a single scalar string like:

- ../../base
 - ../../shared/prod
- m3.yaml

Parsers (gopkg.in/yaml.v3 and sigs.k8s.io/yaml) both load this into:

[]string{"../../base - ../../shared/prod", "m3.yaml"}

So only two list entries exist.

When Kustomize builds this overlay:
• accumulateFile("../../base - ../../shared/prod") fails (because /shared/prod is a directory).
• ldr, err := kt.ldr.New("../../base - ../../shared/prod") succeeds, because FileLoader.New calls
filesys.ConfirmDir(fl.fSys, fl.root.Join(path))
and filepath.Join("/overlays/prod1", "../../base - ../../shared/prod") normalizes to /shared/prod.
• As a result, Kustomize recurses into /shared/prod/kustomization.yaml and accumulates u2.
• The intended ../../base is completely lost, but no error is surfaced.

The final build contains manifests from ../../shared/prod and u3, but the ones in ../../base are missing.

This is quite dangerous especially in cicd environments using flux where resources can accidentally be deleted if the manifests are not rendered.

Minimal Reproduction

Self-contained Go program showing YAML parsing and build behavior:


import (
	"fmt"
	"log"
	"strings"

	"sigs.k8s.io/kustomize/api/krusty"
	"sigs.k8s.io/kustomize/kyaml/filesys"
	syaml "sigs.k8s.io/yaml"
)

const malformed = `
kind: Kustomization
resources:
- ../../base
 - ../../shared/prod
- m3.yaml
`

type K struct {
	Resources []string `yaml:"resources"`
}

func main() {
	// Show how YAML parses
	var k K
	if err := syaml.Unmarshal([]byte(malformed), &k); err != nil {
		panic(err)
	}
	fmt.Println("Parsed resources:", k.Resources)
	// → ["../../base - ../../shared/prod", "m3.yaml"]

	// Build an in-memory FS with base, shared/prod, overlay
	fsys := filesys.MakeFsInMemory()
	write := func(path, content string) {
		if err := fsys.MkdirAll(filepath.Dir(path)); err != nil { log.Fatal(err) }
		if err := fsys.WriteFile(path, []byte(content)); err != nil { log.Fatal(err) }
	}
	write("base/kustomization.yaml", "kind: Kustomization\nresources: [m1.yaml]\n")
	write("base/m1.yaml", `apiVersion: v1
kind: Pod
metadata: {name: u1}
spec: {containers: [{name: c, image: alpine}]}`)
	write("shared/prod/kustomization.yaml", "kind: Kustomization\nresources: [m2.yaml]\n")
	write("shared/prod/m2.yaml", `apiVersion: v1
kind: Pod
metadata: {name: u2}
spec: {containers: [{name: c, image: alpine}]}`)
	write("overlays/prod1/m3.yaml", `apiVersion: v1
kind: Pod
metadata: {name: u3}
spec: {containers: [{name: c, image: alpine}]}`)
	write("overlays/prod1/kustomization.yaml", malformed)

	kz := krusty.MakeKustomizer(krusty.MakeDefaultOptions())
	rm, err := kz.Run(fsys, "overlays/prod1")
	if err != nil { log.Fatal(err) }

	for _, r := range rm.Resources() {
		fmt.Println("Built:", r.GetName())
	}
	// Output: u2, u3  (u1 is missing!)
}

Observed behavior
• resources entry is parsed as a single string.
• Kustomize silently turns "../../base - ../../shared/prod" into a loader rooted at /shared/prod.
• /shared/prod/kustomization.yaml is loaded, so u2 appears.
• ../../base is lost, and no error is raised.

Expected behavior
• Kustomize should error out if a resources entry does not resolve to an existing file or directory.
• It should not silently normalize a malformed string into the “nearest existing directory.”

Root cause

FileLoader.New(path):
root, err := filesys.ConfirmDir(fl.fSys, fl.root.Join(path))

Here fl.root.Join("../../base - ../../shared/prod") → filepath.Join("/overlays/prod1", "../../base - ../../shared/prod")
= /shared/prod after filepath.Clean.

So ConfirmDir returns /shared/prod instead of erroring, and Kustomize treats it as valid.

Suggested fix

Tighten FileLoader.New (or ConfirmDir) so that:
• If path does not resolve exactly to an existing directory under the current root, error.
• Do not allow “ancestor snapping” caused by filepath.Clean and .. removal.

For example:
abs := fl.root.Join(path) if !fl.fSys.Exists(abs) { return nil, fmt.Errorf("new root %q does not exist", path) } if !fl.fSys.IsDir(abs) { return nil, fmt.Errorf("new root %q must be a directory", path) }

Environment
• kustomize/api v0.20.1
• Go 1.22
• Reproducible with in-memory FS and also on disk.

Impact: malformed YAML in resources: can cause Kustomize to silently skip one base and load another, leading to missing resources without any error.

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-kindIndicates a PR lacks a `kind/foo` label and requires one.triage/acceptedIndicates an issue or PR is ready to be actively worked on.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions