Skip to content

Commit ef29872

Browse files
committed
refactor
1 parent 85016af commit ef29872

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+367
-379
lines changed

.golangci.yml

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ linters-settings:
8484
- github.com/unknwon/com: "use gitea's util and replacements"
8585
- io/ioutil: "use os or io instead"
8686
- golang.org/x/exp: "it's experimental and unreliable."
87+
- code.gitea.io/gitea/modules/git/internal: "do not use the internal package, use AddXxx function instead"
8788

8889
issues:
8990
max-issues-per-linter: 0

modules/git/command.go

+61-28
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,20 @@ import (
1616
"time"
1717
"unsafe"
1818

19+
"code.gitea.io/gitea/modules/git/internal" //nolint:depguard // only this file can use the internal type CmdArg, other files and packages should use AddXxx functions
1920
"code.gitea.io/gitea/modules/log"
2021
"code.gitea.io/gitea/modules/process"
2122
"code.gitea.io/gitea/modules/util"
2223
)
2324

25+
// TrustedCmdArgs returns the trusted arguments for git command.
26+
// It's mainly for passing user-provided and trusted arguments to git command
27+
// In most cases, it shouldn't be used. Use AddXxx function instead
28+
type TrustedCmdArgs []internal.CmdArg
29+
2430
var (
2531
// globalCommandArgs global command args for external package setting
26-
globalCommandArgs []CmdArg
32+
globalCommandArgs TrustedCmdArgs
2733

2834
// defaultCommandExecutionTimeout default command execution timeout duration
2935
defaultCommandExecutionTimeout = 360 * time.Second
@@ -42,8 +48,6 @@ type Command struct {
4248
brokenArgs []string
4349
}
4450

45-
type CmdArg string
46-
4751
func (c *Command) String() string {
4852
if len(c.args) == 0 {
4953
return c.name
@@ -53,7 +57,7 @@ func (c *Command) String() string {
5357

5458
// NewCommand creates and returns a new Git Command based on given command and arguments.
5559
// Each argument should be safe to be trusted. User-provided arguments should be passed to AddDynamicArguments instead.
56-
func NewCommand(ctx context.Context, args ...CmdArg) *Command {
60+
func NewCommand(ctx context.Context, args ...internal.CmdArg) *Command {
5761
// Make an explicit copy of globalCommandArgs, otherwise append might overwrite it
5862
cargs := make([]string, 0, len(globalCommandArgs)+len(args))
5963
for _, arg := range globalCommandArgs {
@@ -70,15 +74,9 @@ func NewCommand(ctx context.Context, args ...CmdArg) *Command {
7074
}
7175
}
7276

73-
// NewCommandNoGlobals creates and returns a new Git Command based on given command and arguments only with the specify args and don't care global command args
74-
// Each argument should be safe to be trusted. User-provided arguments should be passed to AddDynamicArguments instead.
75-
func NewCommandNoGlobals(args ...CmdArg) *Command {
76-
return NewCommandContextNoGlobals(DefaultContext, args...)
77-
}
78-
7977
// NewCommandContextNoGlobals creates and returns a new Git Command based on given command and arguments only with the specify args and don't care global command args
8078
// Each argument should be safe to be trusted. User-provided arguments should be passed to AddDynamicArguments instead.
81-
func NewCommandContextNoGlobals(ctx context.Context, args ...CmdArg) *Command {
79+
func NewCommandContextNoGlobals(ctx context.Context, args ...internal.CmdArg) *Command {
8280
cargs := make([]string, 0, len(args))
8381
for _, arg := range args {
8482
cargs = append(cargs, string(arg))
@@ -96,27 +94,62 @@ func (c *Command) SetParentContext(ctx context.Context) *Command {
9694
return c
9795
}
9896

99-
// SetDescription sets the description for this command which be returned on
100-
// c.String()
97+
// SetDescription sets the description for this command which be returned on c.String()
10198
func (c *Command) SetDescription(desc string) *Command {
10299
c.desc = desc
103100
return c
104101
}
105102

106-
// AddArguments adds new git argument(s) to the command. Each argument must be safe to be trusted.
107-
// User-provided arguments should be passed to AddDynamicArguments instead.
108-
func (c *Command) AddArguments(args ...CmdArg) *Command {
103+
func isSafeDynArg(s string) bool {
104+
return s == "" || s[0] != '-'
105+
}
106+
107+
func isValidOption(s string) bool {
108+
return s != "" && s[0] == '-'
109+
}
110+
111+
// AddArguments adds new git arguments to the command. It only accepts string literals, or trusted CmdArg.
112+
// Type CmdArg is in the internal package, so it can not be used outside of this package directly,
113+
// it makes sure that user-provided arguments won't cause RCE risks.
114+
// User-provided arguments should be passed by other AddXxx functions
115+
func (c *Command) AddArguments(args ...internal.CmdArg) *Command {
109116
for _, arg := range args {
110117
c.args = append(c.args, string(arg))
111118
}
112119
return c
113120
}
114121

115-
// AddDynamicArguments adds new dynamic argument(s) to the command.
116-
// The arguments may come from user input and can not be trusted, so no leading '-' is allowed to avoid passing options
122+
// AddOptionValues adds a new option with a list of non-option values
123+
// For example: AddOptionValues("--opt", val) means 2 arguments: {"--opt", val}.
124+
// The values are treated as dynamic arguments. It equals to: AddArguments("--opt") then AddDynamicArguments(val).
125+
func (c *Command) AddOptionValues(opt internal.CmdArg, args ...string) *Command {
126+
if !isValidOption(string(opt)) {
127+
c.brokenArgs = append(c.brokenArgs, string(opt))
128+
return c
129+
}
130+
c.args = append(c.args, string(opt))
131+
c.AddDynamicArguments(args...)
132+
return c
133+
}
134+
135+
// AddOptionFormat adds a new option with a format string and arguments
136+
// For example: AddOptionFormat("--opt=%s %s", val1, val2) means 1 argument: {"--opt=val1 val2"}.
137+
func (c *Command) AddOptionFormat(opt string, args ...any) *Command {
138+
if !isValidOption(opt) {
139+
c.brokenArgs = append(c.brokenArgs, opt)
140+
return c
141+
}
142+
143+
s := fmt.Sprintf(opt, args...)
144+
c.args = append(c.args, s)
145+
return c
146+
}
147+
148+
// AddDynamicArguments adds new dynamic arguments to the command.
149+
// The arguments may come from user input and can not be trusted, so no leading '-' is allowed to avoid passing options.
117150
func (c *Command) AddDynamicArguments(args ...string) *Command {
118151
for _, arg := range args {
119-
if arg != "" && arg[0] == '-' {
152+
if !isSafeDynArg(arg) {
120153
c.brokenArgs = append(c.brokenArgs, arg)
121154
}
122155
}
@@ -137,14 +170,14 @@ func (c *Command) AddDashesAndList(list ...string) *Command {
137170
return c
138171
}
139172

140-
// CmdArgCheck checks whether the string is safe to be used as a dynamic argument.
141-
// It panics if the check fails. Usually it should not be used, it's just for refactoring purpose
142-
// deprecated
143-
func CmdArgCheck(s string) CmdArg {
144-
if s != "" && s[0] == '-' {
145-
panic("invalid git cmd argument: " + s)
173+
// ToTrustedCmdArgs converts a list of strings (trusted as argument) to TrustedCmdArgs
174+
// In most cases, it shouldn't be used. Use AddXxx function instead
175+
func ToTrustedCmdArgs(args []string) TrustedCmdArgs {
176+
ret := make(TrustedCmdArgs, len(args))
177+
for i, arg := range args {
178+
ret[i] = internal.CmdArg(arg)
146179
}
147-
return CmdArg(s)
180+
return ret
148181
}
149182

150183
// RunOpts represents parameters to run the command. If UseContextTimeout is specified, then Timeout is ignored.
@@ -364,9 +397,9 @@ func (c *Command) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunS
364397
}
365398

366399
// AllowLFSFiltersArgs return globalCommandArgs with lfs filter, it should only be used for tests
367-
func AllowLFSFiltersArgs() []CmdArg {
400+
func AllowLFSFiltersArgs() TrustedCmdArgs {
368401
// Now here we should explicitly allow lfs filters to run
369-
filteredLFSGlobalArgs := make([]CmdArg, len(globalCommandArgs))
402+
filteredLFSGlobalArgs := make(TrustedCmdArgs, len(globalCommandArgs))
370403
j := 0
371404
for _, arg := range globalCommandArgs {
372405
if strings.Contains(string(arg), "lfs") {

modules/git/command_test.go

+11
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,14 @@ func TestRunWithContextStd(t *testing.T) {
4141
assert.Empty(t, stderr)
4242
assert.Contains(t, stdout, "git version")
4343
}
44+
45+
func TestGitArgument(t *testing.T) {
46+
assert.True(t, isValidOption("-x"))
47+
assert.True(t, isValidOption("--xx"))
48+
assert.False(t, isValidOption(""))
49+
assert.False(t, isValidOption("x"))
50+
51+
assert.True(t, isSafeDynArg(""))
52+
assert.True(t, isSafeDynArg("x"))
53+
assert.False(t, isSafeDynArg("-x"))
54+
}

modules/git/commit.go

+9-9
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"bytes"
1010
"context"
1111
"errors"
12-
"fmt"
1312
"io"
1413
"os/exec"
1514
"strconv"
@@ -91,8 +90,8 @@ func AddChanges(repoPath string, all bool, files ...string) error {
9190
}
9291

9392
// AddChangesWithArgs marks local changes to be ready for commit.
94-
func AddChangesWithArgs(repoPath string, globalArgs []CmdArg, all bool, files ...string) error {
95-
cmd := NewCommandNoGlobals(append(globalArgs, "add")...)
93+
func AddChangesWithArgs(repoPath string, globalArgs TrustedCmdArgs, all bool, files ...string) error {
94+
cmd := NewCommandContextNoGlobals(DefaultContext, globalArgs...).AddArguments("add")
9695
if all {
9796
cmd.AddArguments("--all")
9897
}
@@ -111,27 +110,28 @@ type CommitChangesOptions struct {
111110
// CommitChanges commits local changes with given committer, author and message.
112111
// If author is nil, it will be the same as committer.
113112
func CommitChanges(repoPath string, opts CommitChangesOptions) error {
114-
cargs := make([]CmdArg, len(globalCommandArgs))
113+
cargs := make(TrustedCmdArgs, len(globalCommandArgs))
115114
copy(cargs, globalCommandArgs)
116115
return CommitChangesWithArgs(repoPath, cargs, opts)
117116
}
118117

119118
// CommitChangesWithArgs commits local changes with given committer, author and message.
120119
// If author is nil, it will be the same as committer.
121-
func CommitChangesWithArgs(repoPath string, args []CmdArg, opts CommitChangesOptions) error {
122-
cmd := NewCommandNoGlobals(args...)
120+
func CommitChangesWithArgs(repoPath string, args TrustedCmdArgs, opts CommitChangesOptions) error {
121+
cmd := NewCommandContextNoGlobals(DefaultContext, args...)
123122
if opts.Committer != nil {
124-
cmd.AddArguments("-c", CmdArg("user.name="+opts.Committer.Name), "-c", CmdArg("user.email="+opts.Committer.Email))
123+
cmd.AddOptionValues("-c", "user.name="+opts.Committer.Name)
124+
cmd.AddOptionValues("-c", "user.email="+opts.Committer.Email)
125125
}
126126
cmd.AddArguments("commit")
127127

128128
if opts.Author == nil {
129129
opts.Author = opts.Committer
130130
}
131131
if opts.Author != nil {
132-
cmd.AddArguments(CmdArg(fmt.Sprintf("--author='%s <%s>'", opts.Author.Name, opts.Author.Email)))
132+
cmd.AddOptionFormat("--author='%s <%s>'", opts.Author.Name, opts.Author.Email)
133133
}
134-
cmd.AddArguments("-m").AddDynamicArguments(opts.Message)
134+
cmd.AddOptionValues("-m", opts.Message)
135135

136136
_, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath})
137137
// No stderr but exit status 1 means nothing to commit.

modules/git/git.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,6 @@ func configUnsetAll(key, value string) error {
383383
}
384384

385385
// Fsck verifies the connectivity and validity of the objects in the database
386-
func Fsck(ctx context.Context, repoPath string, timeout time.Duration, args ...CmdArg) error {
386+
func Fsck(ctx context.Context, repoPath string, timeout time.Duration, args TrustedCmdArgs) error {
387387
return NewCommand(ctx, "fsck").AddArguments(args...).Run(&RunOpts{Timeout: timeout, Dir: repoPath})
388388
}

modules/git/internal/cmdarg.go

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package internal
5+
6+
// CmdArg represents a command argument for git command, and it will be used for the git command directly without any further processing.
7+
// In most cases, you should use the "AddXxx" functions to add arguments, but not use this type directly.
8+
// Casting a risky (user-provided) string to CmdArg would cause security issues if it's injected with a "--xxx" argument.
9+
type CmdArg string

modules/git/repo.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error {
115115
}
116116

117117
// CloneWithArgs original repository to target path.
118-
func CloneWithArgs(ctx context.Context, args []CmdArg, from, to string, opts CloneRepoOptions) (err error) {
118+
func CloneWithArgs(ctx context.Context, args TrustedCmdArgs, from, to string, opts CloneRepoOptions) (err error) {
119119
toDir := path.Dir(to)
120120
if err = os.MkdirAll(toDir, os.ModePerm); err != nil {
121121
return err

modules/git/repo_archive.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ func (repo *Repository) CreateArchive(ctx context.Context, format ArchiveType, t
5757

5858
cmd := NewCommand(ctx, "archive")
5959
if usePrefix {
60-
cmd.AddArguments(CmdArg("--prefix=" + filepath.Base(strings.TrimSuffix(repo.Path, ".git")) + "/"))
60+
cmd.AddOptionFormat("--prefix=%s", filepath.Base(strings.TrimSuffix(repo.Path, ".git"))+"/")
6161
}
62-
cmd.AddArguments(CmdArg("--format=" + format.String()))
62+
cmd.AddOptionFormat("--format=%s" + format.String())
6363
cmd.AddDynamicArguments(commitID)
6464

6565
var stderr strings.Builder

modules/git/repo_attribute.go

+18-21
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
type CheckAttributeOpts struct {
1818
CachedOnly bool
1919
AllAttributes bool
20-
Attributes []CmdArg
20+
Attributes []string
2121
Filenames []string
2222
IndexFile string
2323
WorkTree string
@@ -48,7 +48,7 @@ func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[
4848
} else {
4949
for _, attribute := range opts.Attributes {
5050
if attribute != "" {
51-
cmd.AddArguments(attribute)
51+
cmd.AddDynamicArguments(attribute)
5252
}
5353
}
5454
}
@@ -95,7 +95,7 @@ func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[
9595
// CheckAttributeReader provides a reader for check-attribute content that can be long running
9696
type CheckAttributeReader struct {
9797
// params
98-
Attributes []CmdArg
98+
Attributes []string
9999
Repo *Repository
100100
IndexFile string
101101
WorkTree string
@@ -111,19 +111,6 @@ type CheckAttributeReader struct {
111111

112112
// Init initializes the CheckAttributeReader
113113
func (c *CheckAttributeReader) Init(ctx context.Context) error {
114-
cmdArgs := []CmdArg{"check-attr", "--stdin", "-z"}
115-
116-
if len(c.IndexFile) > 0 {
117-
cmdArgs = append(cmdArgs, "--cached")
118-
c.env = append(c.env, "GIT_INDEX_FILE="+c.IndexFile)
119-
}
120-
121-
if len(c.WorkTree) > 0 {
122-
c.env = append(c.env, "GIT_WORK_TREE="+c.WorkTree)
123-
}
124-
125-
c.env = append(c.env, "GIT_FLUSH=1")
126-
127114
if len(c.Attributes) == 0 {
128115
lw := new(nulSeparatedAttributeWriter)
129116
lw.attributes = make(chan attributeTriple)
@@ -134,11 +121,21 @@ func (c *CheckAttributeReader) Init(ctx context.Context) error {
134121
return fmt.Errorf("no provided Attributes to check")
135122
}
136123

137-
cmdArgs = append(cmdArgs, c.Attributes...)
138-
cmdArgs = append(cmdArgs, "--")
139-
140124
c.ctx, c.cancel = context.WithCancel(ctx)
141-
c.cmd = NewCommand(c.ctx, cmdArgs...)
125+
c.cmd = NewCommand(c.ctx, "check-attr", "--stdin", "-z")
126+
127+
if len(c.IndexFile) > 0 {
128+
c.cmd.AddArguments("--cached")
129+
c.env = append(c.env, "GIT_INDEX_FILE="+c.IndexFile)
130+
}
131+
132+
if len(c.WorkTree) > 0 {
133+
c.env = append(c.env, "GIT_WORK_TREE="+c.WorkTree)
134+
}
135+
136+
c.env = append(c.env, "GIT_FLUSH=1")
137+
138+
c.cmd.AddDynamicArguments(c.Attributes...).AddArguments("--")
142139

143140
var err error
144141

@@ -294,7 +291,7 @@ func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeRe
294291
}
295292

296293
checker := &CheckAttributeReader{
297-
Attributes: []CmdArg{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"},
294+
Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"},
298295
Repo: repo,
299296
IndexFile: indexFilename,
300297
WorkTree: worktree,

modules/git/repo_blame.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
package git
55

6-
import "fmt"
6+
import (
7+
"fmt"
8+
)
79

810
// FileBlame return the Blame object of file
911
func (repo *Repository) FileBlame(revision, path, file string) ([]byte, error) {
@@ -14,8 +16,8 @@ func (repo *Repository) FileBlame(revision, path, file string) ([]byte, error) {
1416
// LineBlame returns the latest commit at the given line
1517
func (repo *Repository) LineBlame(revision, path, file string, line uint) (*Commit, error) {
1618
res, _, err := NewCommand(repo.Ctx, "blame").
17-
AddArguments(CmdArg(fmt.Sprintf("-L %d,%d", line, line))).
18-
AddArguments("-p").AddDynamicArguments(revision).
19+
AddOptionFormat("-L %d,%d", line, line).
20+
AddOptionValues("-p", revision).
1921
AddDashesAndList(file).RunStdString(&RunOpts{Dir: path})
2022
if err != nil {
2123
return nil, err

0 commit comments

Comments
 (0)