From 20e2e258a51a24729bf7047a2afed8d7cd1220ad Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Thu, 16 Jul 2026 23:03:25 -0400 Subject: [PATCH 1/2] security: reject dash-prefixed revisions in RevParse A revision that starts with a dash is interpreted by "git rev-parse" as a command-line option rather than an object, allowing an authenticated user to inject options such as "--absolute-git-dir" through parameters meant for commit SHAs or branch references and leak internal server details to the error logs. The "--end-of-options" separator does not help here because "git rev-parse" does not honor it for revisions, so reject such input outright. RevParse is the single funnel for user-controlled revisions (CatFileCommit, blobs, trees, submodules), so the guard closes the whole class at one place. --- repo.go | 9 +++++++++ repo_test.go | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/repo.go b/repo.go index be3c7d57..18dd80cf 100644 --- a/repo.go +++ b/repo.go @@ -593,6 +593,15 @@ func (r *Repository) RevParse(rev string, opts ...RevParseOptions) (string, erro opt = opts[0] } + // A revision that starts with a dash is never a valid object and would + // instead be interpreted by "git rev-parse" as a command-line option, + // allowing argument injection. The "--end-of-options" separator does not + // help here because "git rev-parse" does not honor it for revisions, so + // reject such input outright. + if strings.HasPrefix(rev, "-") { + return "", ErrRevisionNotExist + } + commitID, err := NewCommand("rev-parse"). AddOptions(opt.CommandOptions). AddArgs(rev). diff --git a/repo_test.go b/repo_test.go index 812faa27..39b05409 100644 --- a/repo_test.go +++ b/repo_test.go @@ -494,6 +494,19 @@ func TestRepository_RevParse(t *testing.T) { expID: "", expErr: ErrRevisionNotExist, }, + + // A revision starting with a dash must be rejected to prevent argument + // injection into "git rev-parse". + { + rev: "--absolute-git-dir", + expID: "", + expErr: ErrRevisionNotExist, + }, + { + rev: "-h", + expID: "", + expErr: ErrRevisionNotExist, + }, } for _, test := range tests { t.Run("", func(t *testing.T) { From 9ef14869f35f67bf26d013860ac232dda376508f Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Thu, 16 Jul 2026 23:11:28 -0400 Subject: [PATCH 2/2] security: pass --end-of-options across remaining git commands Bring the master branch up to parity with v2 by adding the "--end-of-options" separator to the remaining commands that accept it: checkout, reset, fetch, pull, commit, count-objects, fsck, for-each-ref, ls-tree, remote, tag, archive, show (blob), receive-pack, upload-pack, and update-server-info. This hardens them against argument injection as a defense-in-depth measure. --- blob.go | 2 +- commit_archive.go | 1 + repo.go | 18 ++++++++++-------- repo_commit.go | 1 + repo_remote.go | 1 + repo_tag.go | 1 + repo_tree.go | 2 +- server.go | 5 +++-- 8 files changed, 19 insertions(+), 12 deletions(-) diff --git a/blob.go b/blob.go index 9bb85466..381ae7ce 100644 --- a/blob.go +++ b/blob.go @@ -32,5 +32,5 @@ func (b *Blob) Bytes() ([]byte, error) { // Pipeline reads the content of the blob and pipes stdout and stderr to // supplied io.Writer. func (b *Blob) Pipeline(stdout, stderr io.Writer) error { - return NewCommand("show", b.id.String()).RunInDirPipeline(stdout, stderr, b.parent.repo.path) + return NewCommand("show", "--end-of-options", b.id.String()).RunInDirPipeline(stdout, stderr, b.parent.repo.path) } diff --git a/commit_archive.go b/commit_archive.go index cbc45750..692f4ea9 100644 --- a/commit_archive.go +++ b/commit_archive.go @@ -25,6 +25,7 @@ func (c *Commit) CreateArchive(format ArchiveFormat, dst string) error { "--prefix="+prefix, "--format="+string(format), "-o", dst, + "--end-of-options", c.ID.String(), ).RunInDir(c.repo.path) return err diff --git a/repo.go b/repo.go index 18dd80cf..88f66abd 100644 --- a/repo.go +++ b/repo.go @@ -189,6 +189,7 @@ func (r *Repository) Fetch(opts ...FetchOptions) error { if opt.Prune { cmd.AddArgs("--prune") } + cmd.AddArgs("--end-of-options") _, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) return err @@ -229,6 +230,7 @@ func (r *Repository) Pull(opts ...PullOptions) error { if opt.All { cmd.AddArgs("--all") } + cmd.AddArgs("--end-of-options") if !opt.All && opt.Remote != "" { cmd.AddArgs(opt.Remote) if opt.Branch != "" { @@ -300,11 +302,9 @@ func Checkout(repoPath, branch string, opts ...CheckoutOptions) error { cmd := NewCommand("checkout").AddOptions(opt.CommandOptions) if opt.BaseBranch != "" { - cmd.AddArgs("-b") - } - cmd.AddArgs(branch) - if opt.BaseBranch != "" { - cmd.AddArgs(opt.BaseBranch) + cmd.AddArgs("-b", branch, "--end-of-options", opt.BaseBranch) + } else { + cmd.AddArgs("--end-of-options", branch) } _, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath) @@ -348,7 +348,7 @@ func Reset(repoPath, rev string, opts ...ResetOptions) error { cmd.AddArgs("--hard") } - _, err := cmd.AddOptions(opt.CommandOptions).AddArgs(rev).RunInDir(repoPath) + _, err := cmd.AddOptions(opt.CommandOptions).AddArgs("--end-of-options", rev).RunInDir(repoPath) return err } @@ -476,7 +476,8 @@ func CreateCommit(repoPath string, committer *Signature, message string, opts .. } cmd = cmd.AddArgs(fmt.Sprintf("--author='%s <%s>'", opt.Author.Name, opt.Author.Email)). AddArgs("-m", message). - AddOptions(opt.CommandOptions) + AddOptions(opt.CommandOptions). + AddArgs("--end-of-options") _, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath) // No stderr but exit status 1 means nothing to commit. @@ -649,6 +650,7 @@ func CountObjects(repoPath string, opts ...CountObjectsOptions) (*CountObject, e stdout, err := NewCommand("count-objects", "-v"). AddOptions(opt.CommandOptions). + AddArgs("--end-of-options"). RunInDirWithTimeout(opt.Timeout, repoPath) if err != nil { return nil, err @@ -715,7 +717,7 @@ func Fsck(repoPath string, opts ...FsckOptions) error { opt = opts[0] } - cmd := NewCommand("fsck").AddOptions(opt.CommandOptions) + cmd := NewCommand("fsck").AddOptions(opt.CommandOptions).AddArgs("--end-of-options") _, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath) return err } diff --git a/repo_commit.go b/repo_commit.go index d4e8c1fd..37686566 100644 --- a/repo_commit.go +++ b/repo_commit.go @@ -555,6 +555,7 @@ func (r *Repository) LatestCommitTime(opts ...LatestCommitTimeOptions) (time.Tim "--count=1", "--sort=-committerdate", "--format=%(committerdate:iso8601)", + "--end-of-options", ) if opt.Branch != "" { cmd.AddArgs(RefsHeads + opt.Branch) diff --git a/repo_remote.go b/repo_remote.go index 363b8998..50c33be2 100644 --- a/repo_remote.go +++ b/repo_remote.go @@ -216,6 +216,7 @@ func Remotes(repoPath string, opts ...RemotesOptions) ([]string, error) { stdout, err := NewCommand("remote"). AddOptions(opt.CommandOptions). + AddArgs("--end-of-options"). RunInDirWithTimeout(opt.Timeout, repoPath) if err != nil { return nil, err diff --git a/repo_tag.go b/repo_tag.go index fd00deac..29252aa9 100644 --- a/repo_tag.go +++ b/repo_tag.go @@ -184,6 +184,7 @@ func RepoTags(repoPath string, opts ...TagsOptions) ([]string, error) { sorted = true } + cmd.AddArgs("--end-of-options") if opt.Pattern != "" { cmd.AddArgs(opt.Pattern) } diff --git a/repo_tree.go b/repo_tree.go index fe434d41..c21376bc 100644 --- a/repo_tree.go +++ b/repo_tree.go @@ -142,7 +142,7 @@ func (r *Repository) LsTree(treeID string, opts ...LsTreeOptions) (*Tree, error) } stdout, err := cmd. AddOptions(opt.CommandOptions). - AddArgs(treeID). + AddArgs("--end-of-options", treeID). RunInDirWithTimeout(opt.Timeout, r.path) if err != nil { return nil, err diff --git a/server.go b/server.go index 0c2d275e..52f8c427 100755 --- a/server.go +++ b/server.go @@ -33,6 +33,7 @@ func UpdateServerInfo(path string, opts ...UpdateServerInfoOptions) error { if opt.Force { cmd.AddArgs("--force") } + cmd.AddArgs("--end-of-options") _, err := cmd.RunInDirWithTimeout(opt.Timeout, path) return err } @@ -66,7 +67,7 @@ func ReceivePack(path string, opts ...ReceivePackOptions) ([]byte, error) { if opt.HTTPBackendInfoRefs { cmd.AddArgs("--http-backend-info-refs") } - cmd.AddArgs(".") + cmd.AddArgs("--end-of-options", ".") return cmd.RunInDirWithTimeout(opt.Timeout, path) } @@ -108,6 +109,6 @@ func UploadPack(path string, opts ...UploadPackOptions) ([]byte, error) { if opt.HTTPBackendInfoRefs { cmd.AddArgs("--http-backend-info-refs") } - cmd.AddArgs(".") + cmd.AddArgs("--end-of-options", ".") return cmd.RunInDirWithTimeout(opt.Timeout, path) }