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.needs-triageIndicates an issue or PR lacks a `triage/foo` label and requires one.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions