Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: hashicorp/go-getter
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.7.4
Choose a base ref
...
head repository: hashicorp/go-getter
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v1.7.5
Choose a head ref
  • 3 commits
  • 2 files changed
  • 2 contributors

Commits on Jun 20, 2024

  1. recreate git config during update to prevent config alteration

    dduzgun-security committed Jun 20, 2024

    Verified

    This commit was signed with the committer’s verified signature.
    Copy the full SHA
    9906874 View commit details
  2. Copy the full SHA
    5b7ec5f View commit details

Commits on Jun 21, 2024

  1. Merge pull request #497 from hashicorp/fix-git-update

    Prevent Git Config Alteration on Git Update
    dduzgun-security authored Jun 21, 2024
    Copy the full SHA
    5a63fd9 View commit details
Showing with 186 additions and 15 deletions.
  1. +67 −14 get_git.go
  2. +119 −1 get_git_test.go
81 changes: 67 additions & 14 deletions get_git.go
Original file line number Diff line number Diff line change
@@ -125,7 +125,7 @@ func (g *GitGetter) Get(dst string, u *url.URL) error {
return err
}
if err == nil {
err = g.update(ctx, dst, sshKeyFile, ref, depth)
err = g.update(ctx, dst, sshKeyFile, u, ref, depth)
} else {
err = g.clone(ctx, dst, sshKeyFile, u, ref, depth)
}
@@ -228,28 +228,64 @@ func (g *GitGetter) clone(ctx context.Context, dst, sshKeyFile string, u *url.UR
return nil
}

func (g *GitGetter) update(ctx context.Context, dst, sshKeyFile, ref string, depth int) error {
// Determine if we're a branch. If we're NOT a branch, then we just
// switch to master prior to checking out
cmd := exec.CommandContext(ctx, "git", "show-ref", "-q", "--verify", "refs/heads/"+ref)
func (g *GitGetter) update(ctx context.Context, dst, sshKeyFile string, u *url.URL, ref string, depth int) error {
// Remove all variations of .git directories
err := removeCaseInsensitiveGitDirectory(dst)
if err != nil {
return err
}

// Initialize the git repository
cmd := exec.CommandContext(ctx, "git", "init")
cmd.Dir = dst
err = getRunCommand(cmd)
if err != nil {
return err
}

// Add the git remote
cmd = exec.CommandContext(ctx, "git", "remote", "add", "origin", "--", u.String())
cmd.Dir = dst
err = getRunCommand(cmd)
if err != nil {
return err
}

// Fetch the remote ref
cmd = exec.CommandContext(ctx, "git", "fetch", "--tags")
cmd.Dir = dst
err = getRunCommand(cmd)
if err != nil {
return err
}

// Fetch the remote ref
cmd = exec.CommandContext(ctx, "git", "fetch", "origin", "--", ref)
cmd.Dir = dst
err = getRunCommand(cmd)
if err != nil {
return err
}

if getRunCommand(cmd) != nil {
// Not a branch, switch to default branch. This will also catch
// non-existent branches, in which case we want to switch to default
// and then checkout the proper branch later.
ref = findDefaultBranch(ctx, dst)
// Reset the branch to the fetched ref
cmd = exec.CommandContext(ctx, "git", "reset", "--hard", "FETCH_HEAD")
cmd.Dir = dst
err = getRunCommand(cmd)
if err != nil {
return err
}

// We have to be on a branch to pull
if err := g.checkout(ctx, dst, ref); err != nil {
// Checkout ref branch
err = g.checkout(ctx, dst, ref)
if err != nil {
return err
}

// Pull the latest changes from the ref branch
if depth > 0 {
cmd = exec.CommandContext(ctx, "git", "pull", "--depth", strconv.Itoa(depth), "--ff-only")
cmd = exec.CommandContext(ctx, "git", "pull", "origin", "--depth", strconv.Itoa(depth), "--ff-only", "--", ref)
} else {
cmd = exec.CommandContext(ctx, "git", "pull", "--ff-only")
cmd = exec.CommandContext(ctx, "git", "pull", "origin", "--ff-only", "--", ref)
}

cmd.Dir = dst
@@ -377,3 +413,20 @@ func checkGitVersion(ctx context.Context, min string) error {

return nil
}

// removeCaseInsensitiveGitDirectory removes all .git directory variations
func removeCaseInsensitiveGitDirectory(dst string) error {
files, err := os.ReadDir(dst)
if err != nil {
return fmt.Errorf("Failed to read the destination directory %s during git update", dst)
}
for _, f := range files {
if strings.EqualFold(f.Name(), ".git") && f.IsDir() {
err := os.RemoveAll(filepath.Join(dst, f.Name()))
if err != nil {
return fmt.Errorf("Failed to remove the .git directory in the destination directory %s during git update", dst)
}
}
}
return nil
}
120 changes: 119 additions & 1 deletion get_git_test.go
Original file line number Diff line number Diff line change
@@ -169,8 +169,9 @@ func TestGitGetter_remoteWithoutMaster(t *testing.T) {
t.Fatalf("err: %s", err)
}

dst2 := tempDir(t)
// Get again should work
if err := g.Get(dst, repo.url); err != nil {
if err := g.Get(dst2, repo.url); err != nil {
t.Fatalf("err: %s", err)
}

@@ -866,6 +867,123 @@ func TestGitGetter_BadRemoteUrl(t *testing.T) {
}
}

func TestGitGetter_BadGitConfig(t *testing.T) {
if !testHasGit {
t.Log("git not found, skipping")
t.Skip()
}

ctx := context.Background()
g := new(GitGetter)
dst := tempDir(t)

url, err := url.Parse("https://github.com/hashicorp/go-getter")
if err != nil {
t.Fatal(err)
}

_, err = os.Stat(dst)
if err != nil && !os.IsNotExist(err) {
t.Fatalf(err.Error())
}
if err == nil {
// Update the repository containing the bad git config.
// This should remove the bad git config file and initialize a new one.
err = g.update(ctx, dst, testGitToken, url, "main", 1)
} else {
// Clone a repository with a git config file
err = g.clone(ctx, dst, testGitToken, url, "main", 1)
if err != nil {
t.Fatalf(err.Error())
}

// Edit the git config file to simulate a bad git config
gitConfigPath := filepath.Join(dst, ".git", "config")
err = os.WriteFile(gitConfigPath, []byte("bad config"), 0600)
if err != nil {
t.Fatalf(err.Error())
}

// Update the repository containing the bad git config.
// This should remove the bad git config file and initialize a new one.
err = g.update(ctx, dst, testGitToken, url, "main", 1)
}
if err != nil {
t.Fatalf(err.Error())
}

// Check if the .git/config file contains "bad config"
gitConfigPath := filepath.Join(dst, ".git", "config")
configBytes, err := os.ReadFile(gitConfigPath)
if err != nil {
t.Fatalf(err.Error())
}
if strings.Contains(string(configBytes), "bad config") {
t.Fatalf("The .git/config file contains 'bad config'")
}
}

func TestGitGetter_BadGitDirName(t *testing.T) {
if !testHasGit {
t.Log("git not found, skipping")
t.Skip()
}

ctx := context.Background()
g := new(GitGetter)
dst := tempDir(t)

url, err := url.Parse("https://github.com/hashicorp/go-getter")
if err != nil {
t.Fatal(err)
}

_, err = os.Stat(dst)
if err != nil && !os.IsNotExist(err) {
t.Fatalf(err.Error())
}
if err == nil {
// Remove all variations of .git directories
err = removeCaseInsensitiveGitDirectory(dst)
if err != nil {
t.Fatalf(err.Error())
}
} else {
// Clone a repository with a git directory
err = g.clone(ctx, dst, testGitToken, url, "main", 1)
if err != nil {
t.Fatalf(err.Error())
}

// Rename the .git directory to .GIT
oldPath := filepath.Join(dst, ".git")
newPath := filepath.Join(dst, ".GIT")
err = os.Rename(oldPath, newPath)
if err != nil {
t.Fatalf(err.Error())
}

// Remove all variations of .git directories
err = removeCaseInsensitiveGitDirectory(dst)
if err != nil {
t.Fatalf(err.Error())
}
}
if err != nil {
t.Fatalf(err.Error())
}

// Check if the .GIT directory exists
if _, err := os.Stat(filepath.Join(dst, ".GIT")); !os.IsNotExist(err) {
t.Fatalf(".GIT directory still exists")
}

// Check if the .git directory exists
if _, err := os.Stat(filepath.Join(dst, ".git")); !os.IsNotExist(err) {
t.Fatalf(".git directory still exists")
}
}

// gitRepo is a helper struct which controls a single temp git repo.
type gitRepo struct {
t *testing.T