Skip to content

Commit 6d2d86d

Browse files
committed
Resolve bucket object paths with SecureJoin
Bucket object keys are external input and may contain arbitrary characters. Joining them with the reconciler's working directory through `filepath.Join` applies `filepath.Clean`, which collapses parent-directory segments and can yield a destination outside the working directory. `securejoin.SecureJoin` resolves the key while keeping the result within the working directory, matching the pattern already used elsewhere in the controllers for similar joins (e.g. GitRepository include paths). Assisted-by: claude-code/opus-4.7 Signed-off-by: Hidde Beydals <hidde@hhh.computer>
1 parent 3bd3c0e commit 6d2d86d

2 files changed

Lines changed: 45 additions & 1 deletion

File tree

internal/controller/bucket_controller.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"strings"
2828
"time"
2929

30+
securejoin "github.com/cyphar/filepath-securejoin"
3031
"github.com/opencontainers/go-digest"
3132
"golang.org/x/sync/errgroup"
3233
"golang.org/x/sync/semaphore"
@@ -752,7 +753,10 @@ func fetchIndexFiles(ctx context.Context, provider BucketProvider, obj *sourcev1
752753
}
753754
group.Go(func() error {
754755
defer sem.Release(1)
755-
localPath := filepath.Join(tempDir, k)
756+
localPath, err := securejoin.SecureJoin(tempDir, k)
757+
if err != nil {
758+
return fmt.Errorf("failed to resolve path for '%s' object: %w", k, err)
759+
}
756760
etag, err := provider.FGetObject(ctxTimeout, obj.Spec.BucketName, k, localPath)
757761
if err != nil {
758762
if provider.ObjectIsNotFound(err) {

internal/controller/bucket_controller_fetch_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"fmt"
2222
"os"
2323
"path/filepath"
24+
"strings"
2425
"testing"
2526
"time"
2627

@@ -289,4 +290,43 @@ func Test_fetchFiles(t *testing.T) {
289290
t.Fatal(err)
290291
}
291292
})
293+
294+
t.Run("resolves object keys relative to the working directory", func(t *testing.T) {
295+
g := NewWithT(t)
296+
297+
// Place the working directory inside a parent so we can observe
298+
// where files end up on disk.
299+
parent := t.TempDir()
300+
tmp := filepath.Join(parent, "work")
301+
g.Expect(os.Mkdir(tmp, 0o700)).To(Succeed())
302+
303+
client := mockBucketClient{bucketName: bucketName}
304+
client.addObject("../sibling.yaml", mockBucketObject{etag: "etag1", data: "sibling"})
305+
306+
index := client.objectsToDigestIndex()
307+
308+
err := fetchIndexFiles(context.TODO(), client, bucket.DeepCopy(), index, tmp)
309+
g.Expect(err).ToNot(HaveOccurred())
310+
311+
// All fetched files must live under the working directory.
312+
var outside []string
313+
walkErr := filepath.Walk(parent, func(p string, info os.FileInfo, err error) error {
314+
if err != nil {
315+
return err
316+
}
317+
if info.IsDir() {
318+
return nil
319+
}
320+
rel, relErr := filepath.Rel(tmp, p)
321+
if relErr != nil {
322+
return relErr
323+
}
324+
if strings.HasPrefix(rel, "..") {
325+
outside = append(outside, p)
326+
}
327+
return nil
328+
})
329+
g.Expect(walkErr).ToNot(HaveOccurred())
330+
g.Expect(outside).To(BeEmpty(), "files placed outside the working directory: %v", outside)
331+
})
292332
}

0 commit comments

Comments
 (0)