CVE-2024-37904
Description
Minder is an open source Software Supply Chain Security Platform. Minder's Git provider is vulnerable to a denial of service from a maliciously configured GitHub repository. The Git provider clones users repositories using the github.com/go-git/go-git/v5 library on lines L55-L89. The Git provider does the following on the lines L56-L62. First, it sets the CloneOptions, specifying the url, the depth etc. It then validates the options. It then sets up an in-memory filesystem, to which it clones and Finally, it clones the repository. The (g *Git) Clone() method is vulnerable to a DoS attack: A Minder user can instruct Minder to clone a large repository which will exhaust memory and crash the Minder server. The root cause of this vulnerability is a combination of the following conditions: 1. Users can control the Git URL which Minder clones, 2. Minder does not enforce a size limit to the repository, 3. Minder clones the entire repository into memory. This issue has been addressed in commit 7979b43 which has been included in release version v0.0.52. Users are advised to upgrade. There are no known workarounds for this vulnerability.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/stacklok/minderGo | < 0.0.52 | 0.0.52 |
Patches
47979b43328e2a163c667979b43Fix Git.Clone after 35bab8f (#3587)
4 files changed · +38 −20
internal/config/server/provider.go+1 −0 modified@@ -22,6 +22,7 @@ type ProviderConfig struct { Git GitConfig `mapstructure:"git"` } +// GitConfig provides server-side configuration for Git operations like "clone" type GitConfig struct { MaxFiles int64 `mapstructure:"max_files" default:"10000"` MaxBytes int64 `mapstructure:"max_bytes" default:"100_000_000"`
internal/engine/ingester/git/git_test.go+2 −2 modified@@ -17,17 +17,17 @@ package git_test import ( "bytes" "context" - "github.com/stacklok/minder/internal/config/server" - v1 "github.com/stacklok/minder/pkg/providers/v1" "testing" "github.com/stretchr/testify/require" + "github.com/stacklok/minder/internal/config/server" engerrors "github.com/stacklok/minder/internal/engine/errors" gitengine "github.com/stacklok/minder/internal/engine/ingester/git" "github.com/stacklok/minder/internal/providers/credentials" gitclient "github.com/stacklok/minder/internal/providers/git" pb "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" + v1 "github.com/stacklok/minder/pkg/providers/v1" ) func TestGitIngestWithCloneURLFromRepo(t *testing.T) {
internal/providers/git/git.go+19 −8 modified@@ -19,15 +19,14 @@ import ( "context" "errors" "fmt" - "io/fs" "github.com/go-git/go-billy/v5/memfs" - "github.com/go-git/go-git/v5/plumbing/cache" - "github.com/go-git/go-git/v5/storage/filesystem" - "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/cache" "github.com/go-git/go-git/v5/plumbing/transport" + "github.com/go-git/go-git/v5/storage/filesystem" + "github.com/stacklok/minder/internal/config/server" "github.com/stacklok/minder/internal/providers/git/memboxfs" minderv1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" @@ -46,6 +45,7 @@ const maxCachedObjectSize = 100 * 1024 // 100KiB // Ensure that the Git client implements the Git interface var _ provifv1.Git = (*Git)(nil) +// Options implements the "functional options" pattern for Git type Options func(*Git) // NewGit creates a new GitHub client @@ -59,6 +59,7 @@ func NewGit(token provifv1.GitCredential, opts ...Options) *Git { return ret } +// WithConfig configures the Git implementation with server-side configuration options. func WithConfig(cfg server.GitConfig) Options { return func(g *Git) { g.maxFiles = cfg.MaxFiles @@ -97,9 +98,17 @@ func (g *Git) Clone(ctx context.Context, url, branch string) (*git.Repository, e TotalFileSize: g.maxBytes, } } + // go-git seems to want separate filesystems for the storer and the checked out files + storerFs := memfs.New() + if g.maxFiles != 0 && g.maxBytes != 0 { + storerFs = &memboxfs.LimitedFs{ + Fs: storerFs, + MaxFiles: g.maxFiles, + TotalFileSize: g.maxBytes, + } + } storerCache := cache.NewObjectLRU(maxCachedObjectSize) - // reuse same FS for storage and cloned files - storer := filesystem.NewStorage(memFS, storerCache) + storer := filesystem.NewStorage(storerFs, storerCache) // We clone to the memfs go-billy filesystem driver, which doesn't // allow for direct access to the underlying filesystem. This is @@ -112,8 +121,10 @@ func (g *Git) Clone(ctx context.Context, url, branch string) (*git.Repository, e return nil, provifv1.ErrProviderGitBranchNotFound } else if errors.Is(err, transport.ErrEmptyRemoteRepository) { return nil, provifv1.ErrRepositoryEmpty - } else if errors.Is(err, fs.ErrPermission) { - return nil, provifv1.ErrRepositoryTooLarge + } else if errors.Is(err, memboxfs.ErrTooManyFiles) { + return nil, fmt.Errorf("%w: %w", provifv1.ErrRepositoryTooLarge, err) + } else if errors.Is(err, memboxfs.ErrTooBig) { + return nil, fmt.Errorf("%w: %w", provifv1.ErrRepositoryTooLarge, err) } return nil, fmt.Errorf("could not clone repo: %w", err) }
internal/providers/git/memboxfs/fs.go+16 −10 modified@@ -28,17 +28,23 @@ import ( // LimitedFs provides a size-limited billy.Filesystem. This is a struct, there's // no constructor here. Note that LimitedFs is not thread-safe. type LimitedFs struct { - Fs billy.Filesystem + Fs billy.Filesystem MaxFiles int64 TotalFileSize int64 - currentFiles int64 - currentSize int64 + currentFiles int64 + currentSize int64 } // ErrNotImplemented is returned when a method is not implemented. var ErrNotImplemented = fmt.Errorf("not implemented") +// ErrTooBig is returned when a file is too big. +var ErrTooBig = fmt.Errorf("file too big") + +// ErrTooManyFiles is returned when there are too many files. +var ErrTooManyFiles = fmt.Errorf("too many files") + var _ billy.Filesystem = (*LimitedFs)(nil) // Chroot implements billy.Filesystem. @@ -50,7 +56,7 @@ func (_ *LimitedFs) Chroot(_ string) (billy.Filesystem, error) { func (f *LimitedFs) Create(filename string) (billy.File, error) { f.currentFiles++ if f.currentFiles >= f.MaxFiles { - return nil, fs.ErrPermission + return nil, fmt.Errorf("%w: %s", ErrTooManyFiles, filename) } file, err := f.Fs.Create(filename) return &fileWrapper{f: file, fs: f}, err @@ -71,7 +77,7 @@ func (f *LimitedFs) MkdirAll(filename string, perm fs.FileMode) error { // TODO: account for path segments correctly f.currentFiles++ if f.currentFiles >= f.MaxFiles { - return fs.ErrPermission + return fmt.Errorf("%w: %s", ErrTooManyFiles, filename) } return f.Fs.MkdirAll(filename, perm) } @@ -86,7 +92,7 @@ func (f *LimitedFs) OpenFile(filename string, flag int, perm fs.FileMode) (billy if flag&os.O_CREATE != 0 { f.currentFiles++ if f.currentFiles >= f.MaxFiles { - return nil, fs.ErrPermission + return nil, fmt.Errorf("%w: %s", ErrTooManyFiles, filename) } } file, err := f.Fs.OpenFile(filename, flag, perm) @@ -129,7 +135,7 @@ func (f *LimitedFs) Stat(filename string) (fs.FileInfo, error) { func (f *LimitedFs) Symlink(target string, link string) error { f.currentFiles++ if f.currentFiles >= f.MaxFiles { - return fs.ErrPermission + return fmt.Errorf("%w: %s", ErrTooManyFiles, link) } return f.Fs.Symlink(target, link) } @@ -138,7 +144,7 @@ func (f *LimitedFs) Symlink(target string, link string) error { func (f *LimitedFs) TempFile(dir string, prefix string) (billy.File, error) { f.currentFiles++ if f.currentFiles >= f.MaxFiles { - return nil, fs.ErrPermission + return nil, fmt.Errorf("%w: %s/%s", ErrTooManyFiles, dir, prefix) } file, err := f.Fs.TempFile(dir, prefix) return &fileWrapper{f: file, fs: f}, err @@ -191,7 +197,7 @@ func (f *fileWrapper) Truncate(size int64) error { growth := size - existingSize if growth+f.fs.currentSize > f.fs.TotalFileSize { - return fs.ErrPermission + return fmt.Errorf("%w: %s", ErrTooBig, f.Name()) } f.fs.currentSize += growth @@ -219,7 +225,7 @@ func (f *fileWrapper) Write(p []byte) (n int, err error) { growth = 0 } if growth+f.fs.currentSize > f.fs.TotalFileSize { - return 0, fs.ErrPermission + return 0, fmt.Errorf("%w: %s", ErrTooBig, f.Name()) } f.fs.currentSize += growth
35bab8f9a602Merge pull request from GHSA-hpcg-xjq5-g666
20 files changed · +402 −26
cmd/dev/app/image/cmd_verify.go+1 −0 modified@@ -112,6 +112,7 @@ func runCmdVerify(cmd *cobra.Command, _ []string) error { func buildGitHubClient(token string) (provifv1.GitHub, error) { return clients.NewRestClient( &minderv1.GitHubProviderConfig{}, + nil, &ratecache.NoopRestClientCache{}, credentials.NewGitHubTokenCredential(token), clients.NewGitHubClientFactory(telemetry.NewNoopMetrics()),
cmd/dev/app/rule_type/rttst.go+3 −1 modified@@ -302,7 +302,9 @@ func getProvider(pstr string, token string, providerConfigFile string) (provifv1 case "github": client, err := clients.NewGitHubAppProvider( &minderv1.GitHubAppProviderConfig{}, - &serverconfig.GitHubAppConfig{AppName: "test"}, + &serverconfig.ProviderConfig{ + GitHubApp: &serverconfig.GitHubAppConfig{AppName: "test"}, + }, &ratecache.NoopRestClientCache{}, credentials.NewGitHubTokenCredential(token), nil,
internal/config/server/provider.go+6 −0 modified@@ -19,4 +19,10 @@ package server type ProviderConfig struct { GitHubApp *GitHubAppConfig `mapstructure:"github-app"` GitHub *GitHubConfig `mapstructure:"github"` + Git GitConfig `mapstructure:"git"` +} + +type GitConfig struct { + MaxFiles int64 `mapstructure:"max_files" default:"10000"` + MaxBytes int64 `mapstructure:"max_bytes" default:"100_000_000"` }
internal/config/utils.go+2 −2 modified@@ -258,7 +258,7 @@ func getDefaultValueForInt64(value string) (any, error) { var defaultValue any var err error - defaultValue, err = strconv.Atoi(value) + defaultValue, err = strconv.ParseInt(value, 0, 0) if err == nil { return defaultValue, nil } @@ -285,7 +285,7 @@ func getDefaultValue(field reflect.StructField, value string, valueName string) defaultValue, err = getDefaultValueForInt64(value) case reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint: - defaultValue, err = strconv.Atoi(value) + defaultValue, err = strconv.ParseInt(value, 0, 0) case reflect.Float64: defaultValue, err = strconv.ParseFloat(value, 64) case reflect.Bool:
internal/engine/actions/remediate/gh_branch_protect/gh_branch_protect_test.go+1 −0 modified@@ -56,6 +56,7 @@ func testGithubProvider(baseURL string) (provifv1.GitHub, error) { &pb.GitHubProviderConfig{ Endpoint: baseURL, }, + nil, &ratecache.NoopRestClientCache{}, credentials.NewGitHubTokenCredential("token"), clients.NewGitHubClientFactory(telemetry.NewNoopMetrics()),
internal/engine/actions/remediate/pull_request/pull_request_test.go+1 −0 modified@@ -93,6 +93,7 @@ func testGithubProvider() (provifv1.GitHub, error) { &pb.GitHubProviderConfig{ Endpoint: ghApiUrl + "/", }, + nil, &ratecache.NoopRestClientCache{}, credentials.NewGitHubTokenCredential("token"), clients.NewGitHubClientFactory(telemetry.NewNoopMetrics()),
internal/engine/actions/remediate/rest/rest_test.go+1 −0 modified@@ -56,6 +56,7 @@ func testGithubProvider(baseURL string) (provifv1.REST, error) { &pb.GitHubProviderConfig{ Endpoint: baseURL, }, + nil, &ratecache.NoopRestClientCache{}, credentials.NewGitHubTokenCredential("token"), clients.NewGitHubClientFactory(telemetry.NewNoopMetrics()),
internal/engine/ingester/artifact/artifact_test.go+1 −0 modified@@ -48,6 +48,7 @@ func testGithubProvider() (provinfv1.GitHub, error) { &pb.GitHubProviderConfig{ Endpoint: baseURL, }, + nil, &ratecache.NoopRestClientCache{}, credentials.NewGitHubTokenCredential("token"), clients.NewGitHubClientFactory(telemetry.NewNoopMetrics()),
internal/engine/ingester/git/git_test.go+68 −1 modified@@ -17,6 +17,8 @@ package git_test import ( "bytes" "context" + "github.com/stacklok/minder/internal/config/server" + v1 "github.com/stacklok/minder/pkg/providers/v1" "testing" "github.com/stretchr/testify/require" @@ -31,9 +33,18 @@ import ( func TestGitIngestWithCloneURLFromRepo(t *testing.T) { t.Parallel() + cfg := server.GitConfig{ + MaxFiles: 100, + MaxBytes: 1_000_000, + } + gitClient := gitclient.NewGit( + credentials.NewEmptyCredential(), + gitclient.WithConfig(cfg), + ) + gi, err := gitengine.NewGitIngester( &pb.GitType{Branch: "master"}, - gitclient.NewGit(credentials.NewEmptyCredential()), + gitClient, ) require.NoError(t, err, "expected no error") @@ -193,3 +204,59 @@ func TestGitIngestFailsBecauseOfUnexistentCloneUrl(t *testing.T) { require.Error(t, err, "expected error") require.Nil(t, got, "expected nil result") } + +func TestGitIngestFailsWhenRepoTooLarge(t *testing.T) { + t.Parallel() + + // set size limit to 1 byte + cfg := server.GitConfig{ + MaxFiles: 100, + MaxBytes: 1, + } + gitClient := gitclient.NewGit( + credentials.NewEmptyCredential(), + gitclient.WithConfig(cfg), + ) + + gi, err := gitengine.NewGitIngester( + &pb.GitType{Branch: "master"}, + gitClient, + ) + + require.NoError(t, err, "expected no error") + + got, err := gi.Ingest(context.Background(), &pb.Artifact{}, map[string]any{ + "clone_url": "https://github.com/octocat/Hello-World.git", + }) + require.Error(t, err, "expected error") + require.ErrorIs(t, err, v1.ErrRepositoryTooLarge, "expected ErrRepositoryTooLarge") + require.Nil(t, got, "expected non-nil result") +} + +func TestGitIngestFailsWhenRepoHasTooManyFiles(t *testing.T) { + t.Parallel() + + // will fail because of files in .git + cfg := server.GitConfig{ + MaxFiles: 1, + MaxBytes: 1_000_000, + } + gitClient := gitclient.NewGit( + credentials.NewEmptyCredential(), + gitclient.WithConfig(cfg), + ) + + gi, err := gitengine.NewGitIngester( + &pb.GitType{Branch: "master"}, + gitClient, + ) + + require.NoError(t, err, "expected no error") + + got, err := gi.Ingest(context.Background(), &pb.Artifact{}, map[string]any{ + "clone_url": "https://github.com/octocat/Hello-World.git", + }) + require.Error(t, err, "expected error") + require.ErrorIs(t, err, v1.ErrRepositoryTooLarge, "expected ErrRepositoryTooLarge") + require.Nil(t, got, "expected non-nil result") +}
internal/engine/ingester/ingester_test.go+1 −0 modified@@ -160,6 +160,7 @@ func TestNewRuleDataIngest(t *testing.T) { client, err := clients.NewRestClient( &pb.GitHubProviderConfig{}, + nil, &ratecache.NoopRestClientCache{}, credentials.NewGitHubTokenCredential("token"), clients.NewGitHubClientFactory(telemetry.NewNoopMetrics()),
internal/engine/ingester/rest/rest_test.go+1 −0 modified@@ -114,6 +114,7 @@ func testGithubProviderBuilder(baseURL string) (provifv1.REST, error) { &pb.GitHubProviderConfig{ Endpoint: baseURL, }, + nil, &ratecache.NoopRestClientCache{}, credentials.NewGitHubTokenCredential("token"), clients.NewGitHubClientFactory(telemetry.NewNoopMetrics()),
internal/providers/git/git.go+40 −7 modified@@ -19,30 +19,51 @@ import ( "context" "errors" "fmt" + "io/fs" "github.com/go-git/go-billy/v5/memfs" + "github.com/go-git/go-git/v5/plumbing/cache" + "github.com/go-git/go-git/v5/storage/filesystem" + "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/transport" - "github.com/go-git/go-git/v5/storage/memory" - + "github.com/stacklok/minder/internal/config/server" + "github.com/stacklok/minder/internal/providers/git/memboxfs" minderv1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" provifv1 "github.com/stacklok/minder/pkg/providers/v1" ) // Git is the struct that contains the GitHub REST API client type Git struct { credential provifv1.GitCredential + maxFiles int64 + maxBytes int64 } +const maxCachedObjectSize = 100 * 1024 // 100KiB + // Ensure that the Git client implements the Git interface var _ provifv1.Git = (*Git)(nil) +type Options func(*Git) + // NewGit creates a new GitHub client -func NewGit(token provifv1.GitCredential) *Git { - return &Git{ +func NewGit(token provifv1.GitCredential, opts ...Options) *Git { + ret := &Git{ credential: token, } + for _, opt := range opts { + opt(ret) + } + return ret +} + +func WithConfig(cfg server.GitConfig) Options { + return func(g *Git) { + g.maxFiles = cfg.MaxFiles + g.maxBytes = cfg.MaxBytes + } } // CanImplement returns true/false depending on whether the Provider @@ -67,20 +88,32 @@ func (g *Git) Clone(ctx context.Context, url, branch string) (*git.Repository, e return nil, fmt.Errorf("invalid clone options: %w", err) } - storer := memory.NewStorage() - fs := memfs.New() + // TODO(#3582): Switch this to use a tmpfs backed clone + memFS := memfs.New() + if g.maxFiles != 0 && g.maxBytes != 0 { + memFS = &memboxfs.LimitedFs{ + Fs: memFS, + MaxFiles: g.maxFiles, + TotalFileSize: g.maxBytes, + } + } + storerCache := cache.NewObjectLRU(maxCachedObjectSize) + // reuse same FS for storage and cloned files + storer := filesystem.NewStorage(memFS, storerCache) // We clone to the memfs go-billy filesystem driver, which doesn't // allow for direct access to the underlying filesystem. This is // because we want to be able to run this in a sandboxed environment // where we don't have access to the underlying filesystem. - r, err := git.CloneContext(ctx, storer, fs, opts) + r, err := git.CloneContext(ctx, storer, memFS, opts) if err != nil { var refspecerr git.NoMatchingRefSpecError if errors.Is(err, git.ErrBranchNotFound) || refspecerr.Is(err) { return nil, provifv1.ErrProviderGitBranchNotFound } else if errors.Is(err, transport.ErrEmptyRemoteRepository) { return nil, provifv1.ErrRepositoryEmpty + } else if errors.Is(err, fs.ErrPermission) { + return nil, provifv1.ErrRepositoryTooLarge } return nil, fmt.Errorf("could not clone repo: %w", err) }
internal/providers/github/clients/app.go+7 −3 modified@@ -80,15 +80,18 @@ func NewAppDelegate( // the GitHubConfig struct func NewGitHubAppProvider( cfg *minderv1.GitHubAppProviderConfig, - appConfig *server.GitHubAppConfig, + appConfig *server.ProviderConfig, restClientCache ratecache.RestClientCache, credential provifv1.GitHubCredential, packageListingClient *gogithub.Client, ghClientFactory GitHubClientFactory, isOrg bool, ) (*github.GitHub, error) { - appName := appConfig.AppName - userId := appConfig.UserID + if appConfig == nil || appConfig.GitHubApp == nil { + return nil, fmt.Errorf("missing GitHub App configuration") + } + appName := appConfig.GitHubApp.AppName + userId := appConfig.GitHubApp.UserID ghClient, delegate, err := ghClientFactory.BuildAppClient( cfg.Endpoint, @@ -107,6 +110,7 @@ func NewGitHubAppProvider( packageListingClient, restClientCache, delegate, + appConfig, ), nil }
internal/providers/github/clients/app_test.go+14 −10 modified@@ -151,7 +151,7 @@ func TestNewGitHubAppProvider(t *testing.T) { client, err := NewGitHubAppProvider( &minderv1.GitHubAppProviderConfig{Endpoint: "https://api.github.com"}, - &config.GitHubAppConfig{}, + &config.ProviderConfig{GitHubApp: &config.GitHubAppConfig{}}, nil, credentials.NewGitHubTokenCredential("token"), github.NewClient(http.DefaultClient), @@ -173,10 +173,12 @@ func TestUserInfo(t *testing.T) { client, err := NewGitHubAppProvider( &minderv1.GitHubAppProviderConfig{Endpoint: "https://api.github.com"}, - &config.GitHubAppConfig{ - AppName: appName, - AppID: appId, - UserID: expectedUserId, + &config.ProviderConfig{ + GitHubApp: &config.GitHubAppConfig{ + AppName: appName, + AppID: appId, + UserID: expectedUserId, + }, }, ratecache.NewRestClientCache(context.Background()), credentials.NewGitHubTokenCredential("token"), @@ -252,7 +254,7 @@ func TestArtifactAPIEscapesApp(t *testing.T) { client, err := NewGitHubAppProvider( &minderv1.GitHubAppProviderConfig{Endpoint: testServer.URL + "/"}, - &config.GitHubAppConfig{}, + &config.ProviderConfig{GitHubApp: &config.GitHubAppConfig{}}, ratecache.NewRestClientCache(context.Background()), credentials.NewGitHubTokenCredential("token"), packageListingClient, @@ -339,8 +341,10 @@ func TestListPackagesByRepository(t *testing.T) { provider, err := NewGitHubAppProvider( &minderv1.GitHubAppProviderConfig{}, - &config.GitHubAppConfig{ - FallbackToken: accessToken, + &config.ProviderConfig{ + GitHubApp: &config.GitHubAppConfig{ + FallbackToken: accessToken, + }, }, ratecache.NewRestClientCache(context.Background()), credentials.NewGitHubTokenCredential(accessToken), @@ -389,7 +393,7 @@ func TestWaitForRateLimitResetApp(t *testing.T) { client, err := NewGitHubAppProvider( &minderv1.GitHubAppProviderConfig{Endpoint: server.URL + "/"}, - &config.GitHubAppConfig{}, + &config.ProviderConfig{GitHubApp: &config.GitHubAppConfig{}}, ratecache.NewRestClientCache(context.Background()), credentials.NewGitHubTokenCredential(token), packageListingClient, @@ -446,7 +450,7 @@ func TestConcurrentWaitForRateLimitResetApp(t *testing.T) { defer wg.Done() client, err := NewGitHubAppProvider( &minderv1.GitHubAppProviderConfig{Endpoint: server.URL + "/"}, - &config.GitHubAppConfig{}, + &config.ProviderConfig{GitHubApp: &config.GitHubAppConfig{}}, restClientCache, credentials.NewGitHubTokenCredential(token), packageListingClient,
internal/providers/github/clients/oauth.go+3 −0 modified@@ -21,6 +21,7 @@ import ( gogithub "github.com/google/go-github/v61/github" + "github.com/stacklok/minder/internal/config/server" "github.com/stacklok/minder/internal/db" "github.com/stacklok/minder/internal/providers/github" ghcommon "github.com/stacklok/minder/internal/providers/github/common" @@ -77,6 +78,7 @@ func NewOAuthDelegate( // the GitHubConfig struct func NewRestClient( cfg *minderv1.GitHubProviderConfig, + providerCfg *server.ProviderConfig, restClientCache ratecache.RestClientCache, credential provifv1.GitHubCredential, ghClientFactory GitHubClientFactory, @@ -92,6 +94,7 @@ func NewRestClient( ghClient, // use the same client for listing packages and all other operations restClientCache, delegate, + providerCfg, ), nil }
internal/providers/github/clients/oauth_test.go+4 −0 modified@@ -82,6 +82,7 @@ func TestNewRestClient(t *testing.T) { Endpoint: "https://api.github.com", }, nil, + nil, credentials.NewGitHubTokenCredential("token"), NewGitHubClientFactory(provtelemetry.NewNoopMetrics()), "", @@ -136,6 +137,7 @@ func TestArtifactAPIEscapesOAuth(t *testing.T) { client, err := NewRestClient( &minderv1.GitHubProviderConfig{Endpoint: testServer.URL + "/"}, nil, + nil, credentials.NewGitHubTokenCredential("token"), NewGitHubClientFactory(provtelemetry.NewNoopMetrics()), "stacklok", @@ -169,6 +171,7 @@ func TestWaitForRateLimitResetOAuth(t *testing.T) { client, err := NewRestClient( &minderv1.GitHubProviderConfig{Endpoint: server.URL + "/"}, + nil, ratecache.NewRestClientCache(context.Background()), credentials.NewGitHubTokenCredential(token), NewGitHubClientFactory(provtelemetry.NewNoopMetrics()), @@ -219,6 +222,7 @@ func TestConcurrentWaitForRateLimitResetOAuth(t *testing.T) { defer wg.Done() client, err := NewRestClient( &minderv1.GitHubProviderConfig{Endpoint: server.URL + "/"}, + nil, restClientCache, credentials.NewGitHubTokenCredential(token), NewGitHubClientFactory(provtelemetry.NewNoopMetrics()),
internal/providers/github/common.go+8 −1 modified@@ -80,6 +80,7 @@ type GitHub struct { cache ratecache.RestClientCache delegate Delegate ghcrwrap *ghcr.ImageLister + gitConfig config.GitConfig } // Ensure that the GitHub client implements the GitHub interface @@ -156,13 +157,19 @@ func NewGitHub( packageListingClient *github.Client, cache ratecache.RestClientCache, delegate Delegate, + cfg *config.ProviderConfig, ) *GitHub { + var gitConfig config.GitConfig + if cfg != nil { + gitConfig = cfg.Git + } return &GitHub{ client: client, packageListingClient: packageListingClient, cache: cache, delegate: delegate, ghcrwrap: ghcr.FromGitHubClient(client, delegate.GetOwner()), + gitConfig: gitConfig, } } @@ -729,7 +736,7 @@ func (c *GitHub) UpdateIssueComment(ctx context.Context, owner, repo string, num // Clone clones a GitHub repository func (c *GitHub) Clone(ctx context.Context, cloneUrl string, branch string) (*git.Repository, error) { - delegator := gitclient.NewGit(c.delegate.GetCredential()) + delegator := gitclient.NewGit(c.delegate.GetCredential(), gitclient.WithConfig(c.gitConfig)) return delegator.Clone(ctx, cloneUrl, branch) }
internal/providers/github/manager/manager.go+2 −1 modified@@ -119,6 +119,7 @@ func (g *githubProviderManager) Build(ctx context.Context, config *db.Provider) cli, err := clients.NewRestClient( cfg, + nil, g.restClientCache, creds.credential, g.ghClientFactory, @@ -137,7 +138,7 @@ func (g *githubProviderManager) Build(ctx context.Context, config *db.Provider) cli, err := clients.NewGitHubAppProvider( cfg, - g.config.GitHubApp, + g.config, g.restClientCache, creds.credential, g.fallbackTokenClient,
internal/providers/git/memboxfs/fs.go+236 −0 added@@ -0,0 +1,236 @@ +// Copyright 2023 Stacklok, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package memboxfs provides a billy.Fs-compatible filesystem implementation +// which limits the maxiumum size of the in-memory filesystem. +package memboxfs + +import ( + "fmt" + "io" + "io/fs" + "os" + + billy "github.com/go-git/go-billy/v5" +) + +// LimitedFs provides a size-limited billy.Filesystem. This is a struct, there's +// no constructor here. Note that LimitedFs is not thread-safe. +type LimitedFs struct { + Fs billy.Filesystem + MaxFiles int64 + TotalFileSize int64 + + currentFiles int64 + currentSize int64 +} + +// ErrNotImplemented is returned when a method is not implemented. +var ErrNotImplemented = fmt.Errorf("not implemented") + +var _ billy.Filesystem = (*LimitedFs)(nil) + +// Chroot implements billy.Filesystem. +func (_ *LimitedFs) Chroot(_ string) (billy.Filesystem, error) { + return nil, ErrNotImplemented +} + +// Create implements billy.Filesystem. +func (f *LimitedFs) Create(filename string) (billy.File, error) { + f.currentFiles++ + if f.currentFiles >= f.MaxFiles { + return nil, fs.ErrPermission + } + file, err := f.Fs.Create(filename) + return &fileWrapper{f: file, fs: f}, err +} + +// Join implements billy.Filesystem. +func (f *LimitedFs) Join(elem ...string) string { + return f.Fs.Join(elem...) +} + +// Lstat implements billy.Filesystem. +func (f *LimitedFs) Lstat(filename string) (fs.FileInfo, error) { + return f.Fs.Lstat(filename) +} + +// MkdirAll implements billy.Filesystem. +func (f *LimitedFs) MkdirAll(filename string, perm fs.FileMode) error { + // TODO: account for path segments correctly + f.currentFiles++ + if f.currentFiles >= f.MaxFiles { + return fs.ErrPermission + } + return f.Fs.MkdirAll(filename, perm) +} + +// Open implements billy.Filesystem. +func (f *LimitedFs) Open(filename string) (billy.File, error) { + return f.Fs.Open(filename) +} + +// OpenFile implements billy.Filesystem. +func (f *LimitedFs) OpenFile(filename string, flag int, perm fs.FileMode) (billy.File, error) { + if flag&os.O_CREATE != 0 { + f.currentFiles++ + if f.currentFiles >= f.MaxFiles { + return nil, fs.ErrPermission + } + } + file, err := f.Fs.OpenFile(filename, flag, perm) + return &fileWrapper{f: file, fs: f}, err +} + +// ReadDir implements billy.Filesystem. +func (f *LimitedFs) ReadDir(path string) ([]fs.FileInfo, error) { + return f.Fs.ReadDir(path) +} + +// Readlink implements billy.Filesystem. +func (f *LimitedFs) Readlink(link string) (string, error) { + return f.Fs.Readlink(link) +} + +// Remove implements billy.Filesystem. +func (f *LimitedFs) Remove(filename string) error { + // TODO: should we decrement currentFiles here? It's not clear if the underlying + // fs will reclaim memory on Remove, so we are conservative. + return f.Fs.Remove(filename) +} + +// Rename implements billy.Filesystem. +func (f *LimitedFs) Rename(oldpath string, newpath string) error { + return f.Fs.Rename(oldpath, newpath) +} + +// Root implements billy.Filesystem. +func (f *LimitedFs) Root() string { + return f.Fs.Root() +} + +// Stat implements billy.Filesystem. +func (f *LimitedFs) Stat(filename string) (fs.FileInfo, error) { + return f.Fs.Stat(filename) +} + +// Symlink implements billy.Filesystem. +func (f *LimitedFs) Symlink(target string, link string) error { + f.currentFiles++ + if f.currentFiles >= f.MaxFiles { + return fs.ErrPermission + } + return f.Fs.Symlink(target, link) +} + +// TempFile implements billy.Filesystem. +func (f *LimitedFs) TempFile(dir string, prefix string) (billy.File, error) { + f.currentFiles++ + if f.currentFiles >= f.MaxFiles { + return nil, fs.ErrPermission + } + file, err := f.Fs.TempFile(dir, prefix) + return &fileWrapper{f: file, fs: f}, err +} + +type fileWrapper struct { + f billy.File + + fs *LimitedFs +} + +var _ billy.File = (*fileWrapper)(nil) + +// Close implements billy.File. +func (f *fileWrapper) Close() error { + return f.f.Close() +} + +// Lock implements billy.File. +func (f *fileWrapper) Lock() error { + return f.f.Lock() +} + +// Name implements billy.File. +func (f *fileWrapper) Name() string { + return f.f.Name() +} + +// Read implements billy.File. +func (f *fileWrapper) Read(p []byte) (n int, err error) { + return f.f.Read(p) +} + +// ReadAt implements billy.File. +func (f *fileWrapper) ReadAt(p []byte, off int64) (n int, err error) { + return f.f.ReadAt(p, off) +} + +// Seek implements billy.File. +func (f *fileWrapper) Seek(offset int64, whence int) (int64, error) { + return f.f.Seek(offset, whence) +} + +// Truncate implements billy.File. +func (f *fileWrapper) Truncate(size int64) error { + existingSize, err := f.fileSize() + if err != nil { + return err + } + + growth := size - existingSize + if growth+f.fs.currentSize > f.fs.TotalFileSize { + return fs.ErrPermission + } + + f.fs.currentSize += growth + return f.f.Truncate(size) +} + +// Unlock implements billy.File. +func (f *fileWrapper) Unlock() error { + return f.f.Unlock() +} + +// Write implements billy.File. +func (f *fileWrapper) Write(p []byte) (n int, err error) { + size, err := f.fileSize() + if err != nil { + return 0, err + } + offset, err := f.Seek(0, io.SeekCurrent) + if err != nil { + return 0, err + } + + growth := offset + int64(len(p)) - size + if growth < 0 { + growth = 0 + } + if growth+f.fs.currentSize > f.fs.TotalFileSize { + return 0, fs.ErrPermission + } + + f.fs.currentSize += growth + return f.f.Write(p) +} + +func (f *fileWrapper) fileSize() (int64, error) { + fi, err := f.fs.Stat(f.Name()) + if err != nil { + return 0, err + } + + return fi.Size(), nil +}
pkg/providers/v1/errors.go+2 −0 modified@@ -21,4 +21,6 @@ var ( ErrProviderGitBranchNotFound = errors.New("branch not found") // ErrRepositoryEmpty is returned when the repository is empty ErrRepositoryEmpty = errors.New("repository is empty") + // ErrRepositoryTooLarge is returned when the configured size limit is exceeded + ErrRepositoryTooLarge = errors.New("repository is too large to clone") )
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-hpcg-xjq5-g666ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-37904ghsaADVISORY
- github.com/stacklok/minder/blob/85985445c8ac3e51f03372e99c7b2f08a6d274aa/internal/providers/git/git.gonvdWEB
- github.com/stacklok/minder/blob/85985445c8ac3e51f03372e99c7b2f08a6d274aa/internal/providers/git/git.gonvdWEB
- github.com/stacklok/minder/commit/35bab8f9a6025eea9e6e3cef6bd80707ac03d2a9ghsaWEB
- github.com/stacklok/minder/commit/7979b43nvdWEB
- github.com/stacklok/minder/security/advisories/GHSA-hpcg-xjq5-g666nvdWEB
News mentions
0No linked articles in our index yet.