Skip to content

Commit 300f361

Browse files
authored
[v0.14] Validate resolved paths stay within the clone directory (#5278)
* Validate resolved paths stay within the clone directory The image scan git commit job now checks that paths specified in the GitRepo spec resolve within the repository clone directory. * Fix flaky HelmURLRegex migration test The "treats second run as no-op" spec manually resets HelmRepoURLRegex after the first migration run using a plain Update on a cached object. The gitjob reconciler can write to the same GitRepo concurrently, leaving the cached resourceVersion stale and causing a 409 Conflict. Wrap the reset in RetryOnConflict with a fresh Get on each attempt, matching the pattern used by other helpers in this suite.
1 parent de84018 commit 300f361

3 files changed

Lines changed: 51 additions & 5 deletions

File tree

integrationtests/gitjob/controller/helm_url_regex_migration_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
k8serrors "k8s.io/apimachinery/pkg/api/errors"
1313
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1414
"k8s.io/apimachinery/pkg/types"
15+
"k8s.io/client-go/util/retry"
1516
)
1617

1718
const (
@@ -254,9 +255,11 @@ var _ = Describe("HelmURLRegex migration", func() {
254255
Expect(getGitRepo("run-twice").Spec.HelmRepoURLRegex).To(Equal(`^https://charts\.example\.com/`))
255256

256257
// Manually reset to simulate a hypothetical re-entry attempt.
257-
gr := getGitRepo("run-twice")
258-
gr.Spec.HelmRepoURLRegex = ""
259-
Expect(k8sClient.Update(ctx, gr)).To(Succeed())
258+
Expect(retry.RetryOnConflict(retry.DefaultRetry, func() error {
259+
gr := getGitRepo("run-twice")
260+
gr.Spec.HelmRepoURLRegex = ""
261+
return k8sClient.Update(ctx, gr)
262+
})).To(Succeed())
260263

261264
// Second run: marker present, migration skipped.
262265
Expect(runHelmURLRegexMigration()).To(Succeed())

internal/cmd/controller/imagescan/gitcommit_job.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,13 @@ func (j *GitCommitJob) cloneAndReplace(ctx context.Context) {
198198
paths = []string{"/"}
199199
}
200200

201-
for _, path := range paths {
202-
updatePath := filepath.Join(tmp, path)
201+
for _, p := range paths {
202+
updatePath := filepath.Clean(filepath.Join(tmp, p))
203+
if pathEscapesBase(tmp, p) {
204+
err = j.updateErrorStatus(ctx, gitrepo, fmt.Errorf("path %q resolves outside the repository root", p))
205+
logger.V(1).Info("Skipping path outside repository root", "path", p, "error", err)
206+
return
207+
}
203208
if err := update.WithSetters(updatePath, updatePath, scans); err != nil {
204209
err = j.updateErrorStatus(ctx, gitrepo, err)
205210
logger.V(1).Info("Cannot update image tags in repo", "error", err)
@@ -260,6 +265,13 @@ func (j *GitCommitJob) updateErrorStatus(ctx context.Context, gitrepo *fleet.Git
260265
return errutil.NewAggregate(merr)
261266
}
262267

268+
// pathEscapesBase reports whether relPath, when resolved relative to base, points outside base.
269+
func pathEscapesBase(base, relPath string) bool {
270+
resolved := filepath.Clean(filepath.Join(base, relPath))
271+
clean := filepath.Clean(base)
272+
return !strings.HasPrefix(resolved, clean+string(os.PathSeparator)) && resolved != clean
273+
}
274+
263275
func shouldSync(gitrepo *fleet.GitRepo) bool {
264276
interval := gitrepo.Spec.ImageSyncInterval
265277
if interval == nil || interval.Seconds() == 0.0 {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package imagescan
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestPathResolutionStaysInsideBase(t *testing.T) {
8+
const base = "/tmp/repo"
9+
tests := []struct {
10+
name string
11+
relPath string
12+
escapes bool
13+
}{
14+
{"plain subdirectory", "manifests", false},
15+
{"nested subdirectory", "charts/my-chart", false},
16+
{"current directory", ".", false},
17+
{"root slash", "/", false},
18+
{"parent traversal", "../etc", true},
19+
{"deep traversal", "../../etc", true},
20+
{"nested then traversal", "sub/../../etc", true},
21+
{"leading-slash traversal", "/../etc", true},
22+
{"absolute path stays inside", "/etc/passwd", false},
23+
}
24+
for _, tt := range tests {
25+
t.Run(tt.name, func(t *testing.T) {
26+
if got := pathEscapesBase(base, tt.relPath); got != tt.escapes {
27+
t.Errorf("pathEscapesBase(%q, %q) = %v, want %v", base, tt.relPath, got, tt.escapes)
28+
}
29+
})
30+
}
31+
}

0 commit comments

Comments
 (0)