Skip to content

Commit cf8f8a6

Browse files
authored
Validate resolved paths stay within the clone directory (#5280)
The image scan git commit job now checks that paths specified in the GitRepo spec resolve within the repository clone directory.
1 parent c68a3da commit cf8f8a6

2 files changed

Lines changed: 45 additions & 2 deletions

File tree

internal/cmd/controller/imagescan/gitcommit_job.go

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

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

267+
// pathEscapesBase reports whether relPath, when resolved relative to base, points outside base.
268+
func pathEscapesBase(base, relPath string) bool {
269+
resolved := filepath.Clean(filepath.Join(base, relPath))
270+
clean := filepath.Clean(base)
271+
return !strings.HasPrefix(resolved, clean+string(os.PathSeparator)) && resolved != clean
272+
}
273+
262274
func shouldSync(gitrepo *fleet.GitRepo) bool {
263275
interval := gitrepo.Spec.ImageSyncInterval
264276
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)