Skip to content

Commit fe61ed9

Browse files
authored
Merge pull request #611 from hashicorp/SECVULN-41053
fix: git checkout ref argument validation
2 parents d533656 + 388f23d commit fe61ed9

File tree

2 files changed

+146
-1
lines changed

2 files changed

+146
-1
lines changed

get_git.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,36 @@ func (g *GitGetter) GetFile(dst string, u *url.URL) error {
174174
}
175175

176176
func (g *GitGetter) checkout(ctx context.Context, dst string, ref string) error {
177-
cmd := exec.CommandContext(ctx, "git", "checkout", ref, "--")
177+
resolvedRef, err := resolveCheckoutRef(ctx, dst, ref)
178+
if err != nil {
179+
return err
180+
}
181+
182+
cmd := exec.CommandContext(ctx, "git", "checkout", resolvedRef)
178183
cmd.Dir = dst
179184
return getRunCommand(cmd)
180185
}
181186

187+
func resolveCheckoutRef(ctx context.Context, dst, ref string) (string, error) {
188+
candidates := []string{
189+
ref,
190+
"refs/remotes/origin/" + ref,
191+
"refs/tags/" + ref,
192+
}
193+
194+
for _, candidate := range candidates {
195+
cmd := exec.CommandContext(ctx, "git", "rev-parse", "--verify", "--quiet", "--end-of-options", candidate+"^{commit}")
196+
cmd.Dir = dst
197+
198+
resolvedRef, err := cmd.Output()
199+
if err == nil {
200+
return strings.TrimSpace(string(resolvedRef)), nil
201+
}
202+
}
203+
204+
return "", fmt.Errorf("invalid ref: %q", ref)
205+
}
206+
182207
// gitCommitIDRegex is a pattern intended to match strings that seem
183208
// "likely to be" git commit IDs, rather than named refs. This cannot be
184209
// an exact decision because it's valid to name a branch or tag after a series

get_git_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,126 @@ func TestGitGetter_tag(t *testing.T) {
432432
}
433433
}
434434

435+
func TestGitGetter_checkoutRefOptionInjectionRejected(t *testing.T) {
436+
if !testHasGit {
437+
t.Skip("git not found, skipping")
438+
}
439+
440+
g := new(GitGetter)
441+
repo := testGitRepo(t, "empty-repo")
442+
repo.git("config", "commit.gpgsign", "false")
443+
repo.commitFile("safe.txt", "safe")
444+
445+
secretPath := filepath.Join(t.TempDir(), "secret.txt")
446+
secretLine := "THIS_IS_A_SECRET"
447+
if err := os.WriteFile(secretPath, []byte(secretLine+"\n"), 0600); err != nil {
448+
t.Fatal(err)
449+
}
450+
451+
err := g.checkout(context.Background(), repo.dir, "--pathspec-from-file="+secretPath)
452+
if err == nil {
453+
t.Fatal("checkout succeeded; want error")
454+
}
455+
if !strings.Contains(err.Error(), "invalid ref") {
456+
t.Fatalf("expected invalid ref error, got: %s", err)
457+
}
458+
if strings.Contains(err.Error(), secretLine) {
459+
t.Fatalf("secret leaked in error message:\n%s", err.Error())
460+
}
461+
}
462+
463+
func TestGitGetter_checkoutRefLocalBranchResolved(t *testing.T) {
464+
if !testHasGit {
465+
t.Skip("git not found, skipping")
466+
}
467+
468+
g := new(GitGetter)
469+
repo := testGitRepo(t, "local-branch-repo")
470+
repo.git("config", "commit.gpgsign", "false")
471+
repo.commitFile("base.txt", "base")
472+
repo.git("checkout", "-b", "test-branch")
473+
repo.commitFile("branch.txt", "branch")
474+
wantCommit, err := repo.latestCommit()
475+
if err != nil {
476+
t.Fatal(err)
477+
}
478+
repo.git("checkout", "HEAD~1")
479+
480+
err = g.checkout(context.Background(), repo.dir, "test-branch")
481+
if err != nil {
482+
t.Fatalf("checkout failed: %s", err)
483+
}
484+
485+
gotCommit, err := repo.latestCommit()
486+
if err != nil {
487+
t.Fatal(err)
488+
}
489+
if gotCommit != wantCommit {
490+
t.Fatalf("wrong commit checked out: got %s want %s", gotCommit, wantCommit)
491+
}
492+
}
493+
494+
func TestGitGetter_checkoutRefHEADResolved(t *testing.T) {
495+
if !testHasGit {
496+
t.Skip("git not found, skipping")
497+
}
498+
499+
g := new(GitGetter)
500+
repo := testGitRepo(t, "head-repo")
501+
repo.git("config", "commit.gpgsign", "false")
502+
repo.commitFile("head.txt", "head")
503+
wantCommit, err := repo.latestCommit()
504+
if err != nil {
505+
t.Fatal(err)
506+
}
507+
508+
err = g.checkout(context.Background(), repo.dir, "HEAD")
509+
if err != nil {
510+
t.Fatalf("checkout failed: %s", err)
511+
}
512+
513+
gotCommit, err := repo.latestCommit()
514+
if err != nil {
515+
t.Fatal(err)
516+
}
517+
if gotCommit != wantCommit {
518+
t.Fatalf("wrong commit checked out: got %s want %s", gotCommit, wantCommit)
519+
}
520+
}
521+
522+
func TestGitGetter_checkoutRefShellMetacharactersRejected(t *testing.T) {
523+
if !testHasGit {
524+
t.Skip("git not found, skipping")
525+
}
526+
527+
g := new(GitGetter)
528+
repo := testGitRepo(t, "shell-meta-repo")
529+
repo.git("config", "commit.gpgsign", "false")
530+
repo.commitFile("safe.txt", "safe")
531+
532+
tmpDir := t.TempDir()
533+
markerPath := filepath.Join(tmpDir, "created-by-shell")
534+
secretPath := filepath.Join(tmpDir, "secret.txt")
535+
secretLine := "THIS_IS_A_SECRET"
536+
if err := os.WriteFile(secretPath, []byte(secretLine+"\n"), 0600); err != nil {
537+
t.Fatal(err)
538+
}
539+
540+
err := g.checkout(context.Background(), repo.dir, "main | touch "+markerPath)
541+
if err == nil {
542+
t.Fatal("checkout succeeded; want error")
543+
}
544+
if !strings.Contains(err.Error(), "invalid ref") {
545+
t.Fatalf("expected invalid ref error, got: %s", err)
546+
}
547+
if strings.Contains(err.Error(), secretLine) {
548+
t.Fatalf("secret leaked in error message:\n%s", err.Error())
549+
}
550+
if _, err := os.Stat(markerPath); !os.IsNotExist(err) {
551+
t.Fatalf("shell metacharacters created a file unexpectedly: %v", err)
552+
}
553+
}
554+
435555
func TestGitGetter_GetFile(t *testing.T) {
436556
if !testHasGit {
437557
t.Skip("git not found, skipping")

0 commit comments

Comments
 (0)