CVE-2024-35183 (published 2024-05-15T22:15Z)

wolfictl is a command line tool for working with Wolfi. A git authentication issue in versions prior to 0.16.10 allows a local user’s GitHub token to be sent to remote servers other than `github.com`. Most git-dependent functionality in wolfictl relies on its own `git` package, which contains centralized logic for implementing interactions with git repositories. Some of this functionality requires authentication in order to access private repositories. A central function `GetGitAuth` looks for a GitHub token in the environment variable `GITHUB_TOKEN` and returns it as an HTTP basic auth object to be used with the `github.com/go-git/go-git/v5` library. Most callers (direct or indirect) of `GetGitAuth` use the token to authenticate to github.com only; however, in some cases callers were passing this authentication without checking that the remote git repository was hosted on github.com. This behavior has existed in one form or another since commit 0d06e1578300327c212dda26a5ab31d09352b9d0 - committed January 25, 2023. This impacts anyone who ran the `wolfictl check update` commands with a Melange configuration that included a `git-checkout` directive step that referenced a git repository not hosted on github.com. This also impacts anyone who ran `wolfictl update <url>` with a remote URL outside of github.com. Additionally, these subcommands must have run with the `GITHUB_TOKEN` environment variable set to a valid GitHub token. Users should upgrade to version 0.16.10 to receive a patch.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/wolfi-dev/wolfictl
commit_details: repo_url: https://github.com/wolfi-dev/wolfictl
author: James Rawlings
author_date: 2023-01-25 13:49:36+00:00
msg: wolfictl update: use git auth when cloning melange config repo

Signed-off-by: James Rawlings <jrawlings@chainguard.dev>
repository: repo_url: https://github.com/wolfi-dev/wolfictl
repo_name: wolfi-dev/wolfictl
description: A CLI used to work with the Wolfi OSS project
repo_language: Go
owner: wolfi-dev
file_changes: filename: update.go
change_type: ModificationType.MODIFY
diff: @@ -15,7 +15,7 @@ import (
 func Update(dir, owner, repo, version string, wt *git.Worktree) error {
 
 	// update the .gitmodule config file
-	submodules, err := updateConfigfile(dir, owner, repo, version)
+	submodules, err := updateConfigFile(dir, owner, repo, version)
 	if err != nil {
 		return errors.Wrap(err, "failed to update gitmodules file")
 	}
@@ -30,7 +30,7 @@ func Update(dir, owner, repo, version string, wt *git.Worktree) error {
 	return nil
 }
 
-func updateConfigfile(dir, owner, repo, version string) ([]string, error) {
+func updateConfigFile(dir, owner, repo, version string) ([]string, error) {
 	var submodules []string
 	filename := filepath.Join(dir, ".gitmodules")
 	data, err := os.ReadFile(filename)

programming_language: Go
filename: update_test.go
change_type: ModificationType.MODIFY
diff: @@ -10,17 +10,17 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestSubmodules_update(t *testing.T) {
+func TestSubmodules_updateConfigFile(t *testing.T) {
 
 	dir := t.TempDir()
 
-	data, err := os.ReadFile(filepath.Join("testdata", ".gitmodules"))
+	data, err := os.ReadFile(filepath.Join("testdata", "multiple_submodules", ".gitmodules"))
 	assert.NoError(t, err)
 
 	err = os.WriteFile(filepath.Join(dir, ".gitmodules"), data, 0666)
 	assert.NoError(t, err)
 
-	_, err = updateConfigfile(dir, "foo", "bar", "v1.2.4")
+	_, err = updateConfigFile(dir, "foo", "bar", "v1.2.4")
 	assert.NoError(t, err)
 
 	data, err = os.ReadFile(filepath.Join(dir, ".gitmodules"))

programming_language: Go
filename: update.go
change_type: ModificationType.MODIFY
diff: @@ -104,10 +104,19 @@ func (o *Options) Update() error {
 		defer os.Remove(tempDir)
 	}
 
-	repo, err := git.PlainClone(tempDir, false, &git.CloneOptions{
+	cloneOpts := &git.CloneOptions{
 		URL:      o.RepoURI,
 		Progress: os.Stdout,
-	})
+	}
+	gitToken := os.Getenv("GITHUB_TOKEN")
+	if gitToken != "" {
+		cloneOpts.Auth = &gitHttp.BasicAuth{
+			Username: "abc123",
+			Password: gitToken,
+		}
+	}
+
+	repo, err := git.PlainClone(tempDir, false, cloneOpts)
 	if err != nil {
 		return fmt.Errorf("failed to clone repository %s into %s: %w", o.RepoURI, tempDir, err)
 	}

programming_language: Go
repo_url: https://github.com/wolfi-dev/wolfictl
commit_details: repo_url: https://github.com/wolfi-dev/wolfictl
author: James Rawlings
author_date: 2024-05-14 10:59:26+01:00
msg: Git based commands, only use GITHUB_TOKEN when interacting with GitHub's API

This change also removes the submodules git update as this is not used anymore, therefore does not need updating with the new GetGitAuth signature.

Signed-off-by: James Rawlings <jrawlings@chainguard.dev>
repository: repo_url: https://github.com/wolfi-dev/wolfictl
repo_name: wolfi-dev/wolfictl
description: A CLI used to work with the Wolfi OSS project
repo_language: Go
owner: wolfi-dev
file_changes: filename: data_session.go
change_type: ModificationType.MODIFY
diff: @@ -51,10 +51,15 @@ func NewDataSession(ctx context.Context, opts DataSessionOptions) (*DataSession,
 
 	ds.githubClient = opts.GitHubClient
 
+	gitAuth, err := wgit.GetGitAuth(opts.Distro.Absolute.AdvisoriesHTTPSCloneURL())
+	if err != nil {
+		return nil, fmt.Errorf("getting git auth: %w", err)
+	}
+
 	// clone advisories repo
 	repo, err := git.PlainCloneContext(ctx, tempDir, false, &git.CloneOptions{
 		URL:  opts.Distro.Absolute.AdvisoriesHTTPSCloneURL(),
-		Auth: wgit.GetGitAuth(),
+		Auth: gitAuth,
 	})
 	if err != nil {
 		return nil, fmt.Errorf("cloning advisories repo: %w", err)
@@ -168,9 +173,14 @@ func (ds DataSession) Modified() bool {
 // Push pushes the changes made during the session to the remote advisories
 // repository.
 func (ds DataSession) Push(ctx context.Context) error {
-	err := ds.repo.PushContext(ctx, &git.PushOptions{
+	gitAuth, err := wgit.GetGitAuth(ds.distro.Absolute.AdvisoriesHTTPSCloneURL())
+	if err != nil {
+		return fmt.Errorf("getting git auth: %w", err)
+	}
+
+	err = ds.repo.PushContext(ctx, &git.PushOptions{
 		RemoteURL: ds.distro.Absolute.AdvisoriesHTTPSCloneURL(),
-		Auth:      wgit.GetGitAuth(),
+		Auth:      gitAuth,
 	})
 	if err != nil {
 		return fmt.Errorf("pushing changes: %w", err)

programming_language: Go
filename: git.go
change_type: ModificationType.MODIFY
diff: @@ -2,12 +2,15 @@ package git
 
 import (
 	"fmt"
+	"log/slog"
 	"net/url"
 	"os"
 	"os/exec"
 	"strings"
 	"time"
 
+	"github.com/chainguard-dev/clog"
+
 	"github.com/go-git/go-git/v5/plumbing"
 	"github.com/go-git/go-git/v5/plumbing/object"
 	"github.com/go-git/go-git/v5/plumbing/storer"
@@ -19,20 +22,33 @@ import (
 	gitHttp "github.com/go-git/go-git/v5/plumbing/transport/http"
 )
 
-func GetGitAuth() *gitHttp.BasicAuth {
+func GetGitAuth(gitURL string) (*gitHttp.BasicAuth, error) {
+	logger := clog.NewLogger(slog.Default()) // TODO: plumb through context, everywhere
+
+	parsedURL, err := ParseGitURL(gitURL)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse git URL %q: %w", gitURL, err)
+	}
+
+	// Only use GITHUB_TOKEN for github.com URLs
+	if parsedURL.Host != "github.com" {
+		logger.Warnf("host %q is not github.com, not using GITHUB_TOKEN for authentication", parsedURL.Host)
+		return nil, nil
+	}
+
 	gitToken := os.Getenv("GITHUB_TOKEN")
 
 	if gitToken == "" {
 		// If the token is empty, there's no way we can return a usable authentication
 		// anyway. Whereas if we return nil, and don't auth, we have a chance at
 		// succeeding with access of a public repo.
-		return nil
+		return &gitHttp.BasicAuth{}, nil
 	}
 
 	return &gitHttp.BasicAuth{
 		Username: "abc123",
 		Password: gitToken,
-	}
+	}, nil
 }
 
 type URL struct {
@@ -182,7 +198,10 @@ func TempClone(gitURL, hash string, useAuth bool) (repoDir string, err error) {
 
 	var auth transport.AuthMethod
 	if useAuth {
-		auth = GetGitAuth()
+		auth, err = GetGitAuth(gitURL)
+		if err != nil {
+			return dir, fmt.Errorf("unable to get git auth: %w", err)
+		}
 	}
 
 	repo, err := git.PlainClone(dir, false, &git.CloneOptions{

programming_language: Go
filename: .gitmodules
change_type: ModificationType.DELETE
diff: @@ -1,13 +0,0 @@
-[submodule "images/cheese/mount/bar"]
-	path = images/cheese/mount/bar
-	url = https://github.com/foo/bar.git
-	branch = "v1.2.3"
-# next is a duplicate submodule repo but under a different path, test must update both
-[submodule "images/wine/mount/bar"]
-	path = images/wine/mount/bar
-	url = https://github.com/foo/bar.git
-	branch = "v1.2.3"
-[submodule "images/beer/mount/bar"]
-	path = images/beer/mount/bar
-	url = https://github.com/foo/beer.git
-	branch = "v7.8.9"

programming_language: unknown
filename: update.go
change_type: ModificationType.DELETE
diff: @@ -1,116 +0,0 @@
-package submodules
-
-import (
-	"fmt"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"strings"
-
-	"github.com/go-git/go-git/v5"
-	"github.com/go-git/go-git/v5/config"
-
-	wgit "github.com/wolfi-dev/wolfictl/pkg/git"
-)
-
-// Update will modify a .gitmodules file and perform a `git submodule update --remote`
-func Update(dir, owner, repo, version string, wt *git.Worktree) error {
-	// update the .gitmodule config file
-	submodules, err := updateConfigFile(dir, owner, repo, version)
-	if err != nil {
-		return fmt.Errorf("failed to update gitmodules file: %w", err)
-	}
-
-	if _, err = wt.Add(".gitmodules"); err != nil {
-		return fmt.Errorf("failed to git add .gitmodules: %w", err)
-	}
-
-	// git submodule update --remote
-	for _, submodule := range submodules {
-		err := updateSubmodules(submodule.Name, wt)
-		if err != nil {
-			return fmt.Errorf("failed to update gitmodules: %w", err)
-		}
-
-		// need to fall back to using git CLI as go-git hasn't implemented adding submodules
-		// there are errors with empty dir that are references to git module shas when adding via go-git
-		//nolint:gosec
-		cmd := exec.Command("git", "add", submodule.Path)
-		cmd.Dir = dir
-		err = cmd.Run()
-		if err != nil {
-			return fmt.Errorf("failed to git add %s %w", submodule.Path, err)
-		}
-	}
-
-	return nil
-}
-
-func updateConfigFile(dir, owner, repo, version string) (map[string]*config.Submodule, error) {
-	updatedSubmodules := make(map[string]*config.Submodule)
-
-	filename := filepath.Join(dir, ".gitmodules")
-	data, err := os.ReadFile(filename)
-	if err != nil {
-		return nil, fmt.Errorf("failed to read gitmodules file %s: %w", filename, err)
-	}
-
-	cfg := config.NewModules()
-
-	err = cfg.Unmarshal(data)
-	if err != nil {
-		return updatedSubmodules, err
-	}
-
-	// loop through all submodules in the .gitmodules file and set the version for any matching URLs
-	for k, submodule := range cfg.Submodules {
-		if strings.HasSuffix(submodule.URL, fmt.Sprintf("%s/%s.git", owner, repo)) {
-			submodule.Branch = version
-			updatedSubmodules[k] = submodule
-		}
-	}
-
-	// modify the .gitmodules file if we have updated any submodules
-	if len(updatedSubmodules) > 0 {
-		info, err := os.Stat(filename)
-		if err != nil {
-			return updatedSubmodules, err
-		}
-		output, err := cfg.Marshal()
-		if err != nil {
-			return updatedSubmodules, err
-		}
-		return updatedSubmodules, os.WriteFile(filename, output, info.Mode())
-	}
-	return updatedSubmodules, err
-}
-
-func updateSubmodules(submodule string, wt *git.Worktree) error {
-	sub, err := wt.Submodule(submodule)
-	if err != nil {
-		return err
-	}
-
-	err = sub.Init()
-	if err != nil {
-		return err
-	}
-
-	sr, err := sub.Repository()
-	if err != nil {
-		return err
-	}
-
-	sw, err := sr.Worktree()
-	if err != nil {
-		return err
-	}
-
-	err = sw.Pull(&git.PullOptions{
-		RemoteName: "origin",
-		Auth:       wgit.GetGitAuth(),
-		Depth:      1,
-	})
-
-	return err
-}

programming_language: unknown
filename: update_test.go
change_type: ModificationType.DELETE
diff: @@ -1,34 +0,0 @@
-package submodules
-
-import (
-	"os"
-	"path/filepath"
-	"testing"
-
-	"github.com/go-git/go-git/v5/config"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func TestSubmodules_updateConfigFile(t *testing.T) {
-	dir := t.TempDir()
-
-	data, err := os.ReadFile(filepath.Join("testdata", "multiple_submodules", ".gitmodules"))
-	assert.NoError(t, err)
-
-	err = os.WriteFile(filepath.Join(dir, ".gitmodules"), data, 0o600)
-	assert.NoError(t, err)
-
-	_, err = updateConfigFile(dir, "foo", "bar", "v1.2.4")
-	assert.NoError(t, err)
-
-	data, err = os.ReadFile(filepath.Join(dir, ".gitmodules"))
-	assert.NoError(t, err)
-
-	cfg := config.NewModules()
-	err = cfg.Unmarshal(data)
-	assert.NoError(t, err)
-
-	assert.Equal(t, "v1.2.4", cfg.Submodules["images/cheese/mount/bar"].Branch)
-	assert.Equal(t, "v1.2.4", cfg.Submodules["images/wine/mount/bar"].Branch)
-}

programming_language: unknown
filename: tag.go
change_type: ModificationType.MODIFY
diff: @@ -51,11 +51,16 @@ func PushTag(dir, tagName string) error {
 	}
 	remoteURL := fmt.Sprintf("https://github.com/%s/%s.git", gitURL.Organisation, gitURL.Name)
 
+	gitAuth, err := GetGitAuth(remoteURL)
+	if err != nil {
+		return fmt.Errorf("failed to get git auth: %w", err)
+	}
+
 	po := &git.PushOptions{
 		RemoteName: "origin",
 		RemoteURL:  remoteURL,
 		RefSpecs:   []config.RefSpec{config.RefSpec(fmt.Sprintf("refs/tags/%s:refs/tags/%s", tagName, tagName))},
-		Auth:       GetGitAuth(),
+		Auth:       gitAuth,
 	}
 
 	err = r.Push(po)

programming_language: Go
filename: cleanup.go
change_type: ModificationType.MODIFY
diff: @@ -40,13 +40,18 @@ func gitCheckout(p *config.Pipeline, dir string, mutations map[string]string) er
 		return err
 	}
 
+	gitAuth, err := wgit.GetGitAuth(repoValue)
+	if err != nil {
+		return fmt.Errorf("failed to get git auth: %w", err)
+	}
+
 	cloneOpts := &git.CloneOptions{
 		URL:               repoValue,
 		ReferenceName:     plumbing.ReferenceName(fmt.Sprintf("refs/tags/%s", evaluatedTag)),
 		Progress:          os.Stdout,
 		RecurseSubmodules: git.NoRecurseSubmodules,
 		Depth:             1,
-		Auth:              wgit.GetGitAuth(),
+		Auth:              gitAuth,
 	}
 
 	log.Printf("cloning sources from %s tag %s into a temporary directory '%s', this may take a while", repoValue, dir, evaluatedTag)

programming_language: Go
filename: package.go
change_type: ModificationType.MODIFY
diff: @@ -53,11 +53,16 @@ func (o *PackageOptions) UpdatePackageCmd(ctx context.Context) error {
 		defer os.Remove(tempDir)
 	}
 
+	gitAuth, err := wolfigit.GetGitAuth(o.TargetRepo)
+	if err != nil {
+		return fmt.Errorf("failed to get git auth: %w", err)
+	}
+
 	cloneOpts := &git.CloneOptions{
 		URL:               o.TargetRepo,
 		Progress:          os.Stdout,
 		RecurseSubmodules: git.NoRecurseSubmodules,
-		Auth:              wolfigit.GetGitAuth(),
+		Auth:              gitAuth,
 		Depth:             1,
 	}
 
@@ -119,12 +124,18 @@ func (o *PackageOptions) updateAdvisories(ctx context.Context, repo *git.Reposit
 	if err != nil {
 		return err
 	}
+
+	gitAuth, err := wolfigit.GetGitAuth(gitURL.RawURL)
+	if err != nil {
+		return fmt.Errorf("failed to get git auth: %w", err)
+	}
+
 	// checkout repo into tmp dir so we know we are working on a clean HEAD
 	cloneOpts := &git.CloneOptions{
 		URL:               gitURL.RawURL,
 		RecurseSubmodules: git.NoRecurseSubmodules,
 		ShallowSubmodules: true,
-		Auth:              wolfigit.GetGitAuth(),
+		Auth:              gitAuth,
 		Tags:              git.AllTags,
 		Depth:             20,
 	}

programming_language: Go
filename: update.go
change_type: ModificationType.MODIFY
diff: @@ -3,7 +3,6 @@ package update
 import (
 	"bufio"
 	"context"
-	"errors"
 	"fmt"
 	"log"
 	"net/http"
@@ -25,7 +24,6 @@ import (
 	"github.com/google/uuid"
 	"github.com/wolfi-dev/wolfictl/pkg/gh"
 	wgit "github.com/wolfi-dev/wolfictl/pkg/git"
-	"github.com/wolfi-dev/wolfictl/pkg/git/submodules"
 	http2 "github.com/wolfi-dev/wolfictl/pkg/http"
 	"github.com/wolfi-dev/wolfictl/pkg/melange"
 	"github.com/wolfi-dev/wolfictl/pkg/update/deps"
@@ -135,12 +133,17 @@ func (o *Options) Update(ctx context.Context) error {
 			defer os.Remove(tempDir)
 		}
 
+		gitAuth, err := wgit.GetGitAuth(o.RepoURI)
+		if err != nil {
+			return fmt.Errorf("failed to get git auth: %w", err)
+		}
+
 		cloneOpts := &git.CloneOptions{
 			URL:               o.RepoURI,
 			Progress:          os.Stdout,
 			RecurseSubmodules: git.DefaultSubmoduleRecursionDepth,
 			ShallowSubmodules: true,
-			Auth:              wgit.GetGitAuth(),
+			Auth:              gitAuth,
 			Depth:             1,
 		}
 
@@ -401,20 +404,6 @@ func (o *Options) updateGitPackage(ctx context.Context, repo *git.Repository, pa
 		return fmt.Sprintf("failed to update Makefile: %s", err.Error()), nil
 	}
 
-	// if mapping data has a strip prefix, add it back in to the version for when updating git modules
-	latestVersionWithPrefix := newVersion.Version
-	ghm := o.PackageConfigs[packageName].Config.Update.GitHubMonitor
-	if ghm != nil {
-		if ghm.StripPrefix != "" {
-			latestVersionWithPrefix = ghm.StripPrefix + latestVersionWithPrefix
-		}
-	}
-	// some repos could use git submodules, let's check if a submodule file exists and bump any matching packages
-	err = o.updateGitModules(root, packageName, latestVersionWithPrefix, worktree)
-	if err != nil {
-		return fmt.Sprintf("failed to update git modules: %s", err.Error()), nil
-	}
-
 	// now make sure update config is configured
 	updated, err := config.ParseConfiguration(ctx, filepath.Join(root, pc.Filename))
 	if err != nil {
@@ -534,35 +523,6 @@ func (o *Options) updateMakefile(tempDir, packageName, latestVersion string, wor
 	return nil
 }
 
-// some melange config repos use submodules to pull in git repositories into the source dir before the melange pipelines run
-// this function is a noop if no git submodules exist
-func (o *Options) updateGitModules(dir, packageName, version string, wt *git.Worktree) error {
-	// if no gitmodules file exist this in a noop
-	if _, err := os.Stat(filepath.Join(dir, ".gitmodules")); errors.Is(err, os.ErrNotExist) {
-		return nil
-	}
-
-	ghm := o.PackageConfigs[packageName].Config.Update.GitHubMonitor
-
-	if ghm == nil {
-		o.Logger.Printf("package %s  is not a github repo in mapping data, not attempting to bump gitmodules", packageName)
-		return nil
-	}
-
-	if ghm.Identifier == "" {
-		o.Logger.Printf("no identifier found in mapping data for package %s, not attempting to bump gitmodules", packageName)
-		return nil
-	}
-
-	parts := strings.Split(ghm.Identifier, "/")
-	if len(parts) != 2 {
-		o.Logger.Printf("identifier doesn't look like a github owner/repo in mapping data for package %s, not attempting to bump gitmodules", packageName)
-		return nil
-	}
-
-	return submodules.Update(dir, parts[0], parts[1], version, wt)
-}
-
 // create a unique branch
 func (o *Options) createBranch(repo *git.Repository) (plumbing.ReferenceName, error) {
 	name := uuid.New().String()
@@ -637,11 +597,15 @@ func (o *Options) proposeChanges(ctx context.Context, repo *git.Repository, ref
 	}
 	o.Logger.Printf("proposeChanges: %s git status: %s", packageName, string(rs))
 
+	gitAuth, err := wgit.GetGitAuth(o.RepoURI)
+	if err != nil {
+		return "", fmt.Errorf("failed to get git auth: %w", err)
+	}
+
 	// setup githubReleases auth using standard environment variables
 	pushOpts := &git.PushOptions{
 		RemoteName: "origin",
-		Auth:       wgit.GetGitAuth(),
-		Progress:   os.Stdout, // todo remove if this doesn't help: extra logging to help debug intermittent "object not found" when pushing
+		Auth:       gitAuth,
 	}
 
 	// push the version update changes to our working branch

programming_language: Go