Skip to content

Commit ecb703c

Browse files
authored
fix: Multiple credential-leak paths in gitextractor (#8872)
1 parent aff3487 commit ecb703c

2 files changed

Lines changed: 59 additions & 16 deletions

File tree

backend/plugins/gitextractor/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1414
See the License for the specific language governing permissions and
1515
limitations under the License.
1616
-->
17-
Please see details in the [Apache DevLake website](https://devlake.apache.org/docs/Plugins/gitextractor)
17+
Please see details in the [Apache DevLake website](https://devlake.apache.org/docs/Plugins/gitextractor)
18+
19+
20+
Build with local compiled v1.3.2 libgit2
21+
```
22+
CGO_LDFLAGS="-L/usr/local/lib -lgit2" CGO_CFLAGS="-I/usr/local/include" go build ./plugins/gitextractor/... 2>&1 | tail -5
23+
```

backend/plugins/gitextractor/parser/clone_gitcli.go

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -113,37 +113,40 @@ func (g *GitcliCloner) prepareSync() errors.Error {
113113
}
114114
// support private key
115115
if taskData.Options.PrivateKey != "" {
116-
pkFile, err := os.CreateTemp("", "gitext-pk")
116+
// Create a restricted temp directory so the key file is protected from the moment of creation (CWE-367, CWE-732)
117+
pkDir, err := os.MkdirTemp("", "gitext-pk-")
117118
if err != nil {
118-
g.logger.Error(err, "create temp private key file error")
119+
g.logger.Error(err, "create temp private key dir error")
120+
return errors.Default.New("failed to handle the private key")
121+
}
122+
defer os.RemoveAll(pkDir)
123+
pkFilePath := path.Join(pkDir, "pk")
124+
pkFile, err := os.OpenFile(pkFilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
125+
if err != nil {
126+
g.logger.Error(err, "create private key file error")
119127
return errors.Default.New("failed to handle the private key")
120128
}
121129
if _, e := pkFile.WriteString(taskData.Options.PrivateKey + "\n"); e != nil {
122-
g.logger.Error(err, "write private key file error")
123-
return errors.Default.New("failed to write the private key")
130+
g.logger.Error(e, "write private key file error")
131+
return errors.Default.New("failed to write the private key")
124132
}
125133
pkFile.Close()
126-
if e := os.Chmod(pkFile.Name(), 0600); e != nil {
127-
g.logger.Error(err, "chmod private key file error")
128-
return errors.Default.New("failed to modify the private key")
129-
}
130134

131135
if taskData.Options.Passphrase != "" {
136+
// Pass passphrase via stdin to avoid exposure in /proc/<pid>/cmdline (CWE-214)
132137
pp := exec.CommandContext(
133138
g.ctx.GetContext(),
134139
"ssh-keygen", "-p",
135-
"-P", taskData.Options.Passphrase,
136140
"-N", "",
137-
"-f", pkFile.Name(),
141+
"-f", pkFilePath,
138142
)
139-
if ppout, pperr := pp.CombinedOutput(); pperr != nil {
143+
pp.Stdin = strings.NewReader(taskData.Options.Passphrase + "\n")
144+
if _, pperr := pp.CombinedOutput(); pperr != nil {
140145
g.logger.Error(pperr, "change private key passphrase error")
141-
g.logger.Info(string(ppout))
142146
return errors.Default.New("failed to decrypt the private key")
143147
}
144148
}
145-
defer os.Remove(pkFile.Name())
146-
sshCmdArgs = append(sshCmdArgs, fmt.Sprintf("-i %s -o StrictHostKeyChecking=no", pkFile.Name()))
149+
sshCmdArgs = append(sshCmdArgs, fmt.Sprintf("-i %s -o StrictHostKeyChecking=no", pkFilePath))
147150
}
148151
if len(sshCmdArgs) > 0 {
149152
g.syncEnvs = append(g.syncEnvs, fmt.Sprintf("GIT_SSH_COMMAND=ssh %s", strings.Join(sshCmdArgs, " ")))
@@ -251,6 +254,7 @@ func (g *GitcliCloner) doubleClone() errors.Error {
251254
if e != nil {
252255
return errors.Convert(e)
253256
}
257+
defer os.RemoveAll(intermediaryDir) // CWE-459: ensure cleanup on all exit paths
254258
// step 1: full clone into a intermediary dir
255259
backup := g.localDir
256260
g.localDir = intermediaryDir
@@ -303,7 +307,7 @@ func (g *GitcliCloner) gitCmd(gitcmd string, args ...string) errors.Error {
303307
}
304308

305309
func (g *GitcliCloner) git(env []string, dir string, gitcmd string, args ...string) errors.Error {
306-
g.logger.Debug("git %s %v", gitcmd, args)
310+
g.logger.Debug("git %s %v", gitcmd, sanitizeArgs(args)) // CWE-532: sanitize before logging
307311
args = append([]string{gitcmd}, args...)
308312
cmd := exec.CommandContext(g.ctx.GetContext(), "git", args...)
309313
cmd.Env = env
@@ -335,14 +339,47 @@ func generateErrMsg(output []byte, err error) string {
335339
return errMsg
336340
}
337341

342+
// sensitiveQueryParams lists URL query parameter names that may carry credentials.
343+
var sensitiveQueryParams = []string{"token", "access_token", "private_token", "api_key", "key", "apikey"}
344+
338345
func sanitizeArgs(args []string) []string {
339346
var ret []string
340347
for _, arg := range args {
348+
// Redact Authorization header values (e.g. --header=Authorization: Bearer TOKEN) (CWE-532)
349+
lower := strings.ToLower(arg)
350+
if authIdx := strings.Index(lower, "authorization:"); authIdx != -1 {
351+
colonPos := authIdx + len("authorization:")
352+
rest := strings.TrimSpace(arg[colonPos:])
353+
parts := strings.SplitN(rest, " ", 2)
354+
if len(parts) == 2 {
355+
arg = arg[:colonPos] + " " + parts[0] + " " + strings.Repeat("*", len(parts[1]))
356+
} else if len(rest) > 0 {
357+
arg = arg[:colonPos] + " " + strings.Repeat("*", len(rest))
358+
}
359+
ret = append(ret, arg)
360+
continue
361+
}
341362
u, err := url.Parse(arg)
342363
if err == nil && u != nil && u.User != nil {
343364
password, ok := u.User.Password()
344365
if ok {
366+
// Redact password in user:password@host URLs
345367
arg = strings.Replace(arg, password, strings.Repeat("*", len(password)), -1)
368+
} else {
369+
// Redact username-only tokens (e.g. https://TOKEN@github.com/..., GitHub App pattern)
370+
username := u.User.Username()
371+
if len(username) >= 16 {
372+
arg = strings.Replace(arg, username, strings.Repeat("*", len(username)), -1)
373+
}
374+
}
375+
}
376+
// Redact sensitive query parameters (e.g. ?private_token=..., ?access_token=...)
377+
if err == nil && u != nil && u.RawQuery != "" {
378+
q := u.Query()
379+
for _, param := range sensitiveQueryParams {
380+
if val := q.Get(param); val != "" {
381+
arg = strings.Replace(arg, val, strings.Repeat("*", len(val)), -1)
382+
}
346383
}
347384
}
348385
ret = append(ret, arg)

0 commit comments

Comments
 (0)