Skip to content

Commit c5ab803

Browse files
committed
Address review: security, proxy, tar, zip, subdir, SSH lock
1 parent 2d343e6 commit c5ab803

4 files changed

Lines changed: 63 additions & 13 deletions

File tree

internal/bundlereader/gitclone.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
gitclient "github.com/go-git/go-git/v5/plumbing/transport/client"
2020
httpgit "github.com/go-git/go-git/v5/plumbing/transport/http"
2121
fleetssh "github.com/rancher/fleet/internal/ssh"
22+
fleetgit "github.com/rancher/fleet/pkg/git"
2223
)
2324

2425
// httpsProtocolMu coordinates access to go-git's process-global HTTPS
@@ -84,6 +85,7 @@ func gitDownload(ctx context.Context, dst, rawURL string, auth Auth) error {
8485
cloneOpts := &gogit.CloneOptions{
8586
URL: cloneURL.String(),
8687
InsecureSkipTLS: auth.InsecureSkipVerify,
88+
ProxyOptions: fleetgit.ProxyOptsFromEnvironment(cloneURL.String()),
8789
}
8890
if err := setGitAuth(cloneOpts, &cloneURL, sshKeyPEM, auth); err != nil {
8991
return err
@@ -126,7 +128,7 @@ func gitDownload(ctx context.Context, dst, rawURL string, auth Auth) error {
126128
}
127129
r, err := gogit.PlainCloneContext(ctx, dst, false, cloneOpts)
128130
if err != nil {
129-
return fmt.Errorf("cloning %s: %w", cloneURL.String(), err)
131+
return fmt.Errorf("cloning %s: %w", cloneURL.Redacted(), err)
130132
}
131133
h, err := r.ResolveRevision(plumbing.Revision(ref))
132134
if err != nil {

internal/bundlereader/httpdownload.go

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package bundlereader
33
import (
44
"archive/tar"
55
"archive/zip"
6-
"bytes"
76
"compress/gzip"
87
"context"
98
"errors"
@@ -141,6 +140,8 @@ func extractTar(dst string, r io.Reader) error {
141140
if err := os.Symlink(hdr.Linkname, target); err != nil && !os.IsExist(err) {
142141
return err
143142
}
143+
default:
144+
return fmt.Errorf("unsupported tar entry type %d for %q", hdr.Typeflag, hdr.Name)
144145
}
145146
}
146147
return nil
@@ -169,14 +170,33 @@ func extractGz(dst, name string, r io.Reader) error {
169170
return err
170171
}
171172

172-
// extractZipFromReader downloads the zip body to memory and extracts it.
173-
// archive/zip requires a ReaderAt, so we buffer the response first.
173+
// extractZipFromReader streams the zip body to a temporary file and extracts it.
174+
// archive/zip requires a ReaderAt, so we use an *os.File as the backing store
175+
// to avoid buffering the entire archive in memory.
174176
func extractZipFromReader(dst string, r io.Reader) error {
175-
data, err := io.ReadAll(r)
177+
f, err := os.CreateTemp("", "bundle-*.zip")
176178
if err != nil {
177-
return fmt.Errorf("reading zip body: %w", err)
179+
return fmt.Errorf("creating temp file for zip: %w", err)
178180
}
179-
zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
181+
defer func() {
182+
f.Close()
183+
os.Remove(f.Name())
184+
}()
185+
186+
if _, err := io.Copy(f, r); err != nil {
187+
return fmt.Errorf("writing zip body to temp file: %w", err)
188+
}
189+
190+
info, err := f.Stat()
191+
if err != nil {
192+
return fmt.Errorf("stat temp zip file: %w", err)
193+
}
194+
195+
if _, err := f.Seek(0, io.SeekStart); err != nil {
196+
return fmt.Errorf("seeking temp zip file: %w", err)
197+
}
198+
199+
zr, err := zip.NewReader(f, info.Size())
180200
if err != nil {
181201
return fmt.Errorf("opening zip: %w", err)
182202
}

internal/bundlereader/loaddirectory.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,27 @@ func downloadOCIChart(name, version, path string, auth Auth) (string, error) {
371371
return saved, nil
372372
}
373373

374+
// safeJoinSubDir joins base and sub, returning an error if sub is absolute or
375+
// escapes base (e.g., due to ".." components).
376+
func safeJoinSubDir(base, sub string) (string, error) {
377+
cleanSub := filepath.Clean(filepath.FromSlash(sub))
378+
if filepath.IsAbs(cleanSub) {
379+
return "", fmt.Errorf("subdir must be relative, got %q", sub)
380+
}
381+
if cleanSub == "." {
382+
return base, nil
383+
}
384+
joined := filepath.Join(base, cleanSub)
385+
rel, err := filepath.Rel(base, joined)
386+
if err != nil {
387+
return "", err
388+
}
389+
if rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
390+
return "", fmt.Errorf("subdir %q escapes base directory", sub)
391+
}
392+
return joined, nil
393+
}
394+
374395
// fetchToDir resolves source (relative to pwd), downloads or copies it, and
375396
// places the resulting files into dst.
376397
func fetchToDir(ctx context.Context, dst, source, pwd string, auth Auth) error {
@@ -392,7 +413,10 @@ func fetchToDir(ctx context.Context, dst, source, pwd string, auth Auth) error {
392413
return err
393414
}
394415

395-
src := filepath.Join(td, filepath.FromSlash(si.subDir))
416+
src, err := safeJoinSubDir(td, si.subDir)
417+
if err != nil {
418+
return fmt.Errorf("invalid subdir %q: %w", si.subDir, err)
419+
}
396420
if err := os.MkdirAll(dst, 0750); err != nil {
397421
return err
398422
}

internal/cmd/cli/gitcloner/cloner.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,13 @@ var caCloneMutex sync.RWMutex
4343

4444
// withExclusiveCA installs a custom HTTPS transport that trusts only caBundle
4545
// (not the system cert pool) for the duration of fn, then restores the default.
46-
// When caBundle is empty, fn is called under a shared lock so that default
47-
// HTTPS clones cannot run while a custom-CA clone holds the write lock.
48-
func withExclusiveCA(caBundle []byte, insecureSkipTLS bool, fn func() error) error {
46+
// The mutex is only taken for HTTPS URLs because only go-git's HTTPS protocol
47+
// registry is affected; SSH and other scheme clones can proceed concurrently.
48+
func withExclusiveCA(repoURL string, caBundle []byte, insecureSkipTLS bool, fn func() error) error {
49+
if !strings.HasPrefix(repoURL, "https://") {
50+
// Non-HTTPS clone: no protocol registry mutation, no lock needed.
51+
return fn()
52+
}
4953
if len(caBundle) == 0 {
5054
caCloneMutex.RLock()
5155
defer caCloneMutex.RUnlock()
@@ -121,7 +125,7 @@ func (c *Cloner) CloneRepo(opts *GitCloner) error {
121125
}
122126

123127
func cloneBranch(opts *GitCloner, auth transport.AuthMethod, caBundle []byte) error {
124-
return withExclusiveCA(caBundle, opts.InsecureSkipTLS, func() error {
128+
return withExclusiveCA(opts.Repo, caBundle, opts.InsecureSkipTLS, func() error {
125129
r, err := plainClone(opts.Path, false, &git.CloneOptions{
126130
URL: opts.Repo,
127131
Depth: 1,
@@ -147,7 +151,7 @@ func cloneBranch(opts *GitCloner, auth transport.AuthMethod, caBundle []byte) er
147151
}
148152

149153
func cloneRevision(opts *GitCloner, auth transport.AuthMethod, caBundle []byte) error {
150-
return withExclusiveCA(caBundle, opts.InsecureSkipTLS, func() error {
154+
return withExclusiveCA(opts.Repo, caBundle, opts.InsecureSkipTLS, func() error {
151155
r, err := plainClone(opts.Path, false, &git.CloneOptions{
152156
URL: opts.Repo,
153157
Depth: 1,

0 commit comments

Comments
 (0)