Skip to content

Commit 902c156

Browse files
committed
Resolve sparse checkout paths with SecureJoin
When validating that the paths listed in `spec.sparseCheckout` exist in the cloned working tree, resolve each entry with `securejoin.SecureJoin` instead of `filepath.Join`. `filepath.Join` collapses parent-directory segments via `filepath.Clean`, so a configured path like `../foo` would have been checked against a location outside the working tree, masking a missing entry behind an unrelated filesystem stat. SecureJoin keeps the resolved path inside the working tree, matching the pattern already used for include paths elsewhere in the controller. Assisted-by: claude-code/opus-4.7 Signed-off-by: Hidde Beydals <hidde@hhh.computer>
1 parent 6d2d86d commit 902c156

2 files changed

Lines changed: 40 additions & 1 deletion

File tree

internal/controller/gitrepository_controller.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1319,7 +1319,10 @@ func gitContentConfigChanged(obj *sourcev1.GitRepository, includes *artifactSet)
13191319
func (r *GitRepositoryReconciler) validateSparseCheckoutPaths(obj *sourcev1.GitRepository, dir string) error {
13201320
if obj.Spec.SparseCheckout != nil {
13211321
for _, path := range obj.Spec.SparseCheckout {
1322-
fullPath := filepath.Join(dir, path)
1322+
fullPath, err := securejoin.SecureJoin(dir, path)
1323+
if err != nil {
1324+
return fmt.Errorf("sparse checkout dir '%s' cannot be resolved: %w", path, err)
1325+
}
13231326
if _, err := os.Lstat(fullPath); err != nil {
13241327
return fmt.Errorf("sparse checkout dir '%s' does not exist in repository: %w", path, err)
13251328
}

internal/controller/gitrepository_controller_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3101,6 +3101,42 @@ func resetChmod(path string, dirMode os.FileMode, fileMode os.FileMode) error {
31013101
return nil
31023102
}
31033103

3104+
func TestGitRepositoryReconciler_validateSparseCheckoutPaths(t *testing.T) {
3105+
t.Run("succeeds when configured paths exist in the working directory", func(t *testing.T) {
3106+
g := NewWithT(t)
3107+
3108+
dir := t.TempDir()
3109+
g.Expect(os.MkdirAll(filepath.Join(dir, "a", "b"), 0o700)).To(Succeed())
3110+
3111+
obj := &sourcev1.GitRepository{
3112+
Spec: sourcev1.GitRepositorySpec{SparseCheckout: []string{"a/b"}},
3113+
}
3114+
3115+
r := &GitRepositoryReconciler{}
3116+
g.Expect(r.validateSparseCheckoutPaths(obj, dir)).To(Succeed())
3117+
})
3118+
3119+
t.Run("errors when a configured path is not present in the working directory", func(t *testing.T) {
3120+
g := NewWithT(t)
3121+
3122+
// The working directory has nothing in it; a sibling of the working
3123+
// directory exists at the same name as the requested path.
3124+
parent := t.TempDir()
3125+
dir := filepath.Join(parent, "work")
3126+
g.Expect(os.Mkdir(dir, 0o700)).To(Succeed())
3127+
g.Expect(os.MkdirAll(filepath.Join(parent, "sibling"), 0o700)).To(Succeed())
3128+
3129+
obj := &sourcev1.GitRepository{
3130+
Spec: sourcev1.GitRepositorySpec{SparseCheckout: []string{"../sibling"}},
3131+
}
3132+
3133+
r := &GitRepositoryReconciler{}
3134+
err := r.validateSparseCheckoutPaths(obj, dir)
3135+
g.Expect(err).To(HaveOccurred())
3136+
g.Expect(err.Error()).To(ContainSubstring("../sibling"))
3137+
})
3138+
}
3139+
31043140
func TestGitRepositoryIncludeEqual(t *testing.T) {
31053141
tests := []struct {
31063142
name string

0 commit comments

Comments
 (0)