fix: use credentials helpers for git clones

- When cloning with credentials is used, don't set the credentials in
the URL and pass that to Git, instead use Git credential helper to pass
the credential. This avoids the credentials to be leaked through the
process list.
This commit is contained in:
Gusted 2025-08-24 02:30:23 +02:00 committed by Earl Warren
commit f7f7d086e4
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00

View file

@ -18,6 +18,7 @@ import (
"strings" "strings"
"time" "time"
"forgejo.org/modules/log"
"forgejo.org/modules/proxy" "forgejo.org/modules/proxy"
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
"forgejo.org/modules/util" "forgejo.org/modules/util"
@ -160,24 +161,89 @@ func CloneWithArgs(ctx context.Context, args TrustedCmdArgs, from, to string, op
if len(opts.Branch) > 0 { if len(opts.Branch) > 0 {
cmd.AddArguments("-b").AddDynamicArguments(opts.Branch) cmd.AddArguments("-b").AddDynamicArguments(opts.Branch)
} }
cmd.AddDashesAndList(from, to)
if strings.Contains(from, "://") && strings.Contains(from, "@") { envs := os.Environ()
cmd.SetDescription(fmt.Sprintf("clone branch %s from %s to %s (shared: %t, mirror: %t, depth: %d)", opts.Branch, util.SanitizeCredentialURLs(from), to, opts.Shared, opts.Mirror, opts.Depth)) parsedFromURL, err := url.Parse(from)
} else { if err == nil {
cmd.SetDescription(fmt.Sprintf("clone branch %s from %s to %s (shared: %t, mirror: %t, depth: %d)", opts.Branch, from, to, opts.Shared, opts.Mirror, opts.Depth)) envs = proxy.EnvWithProxy(parsedFromURL)
} }
fromURL := from
sanitizedFrom := from
// If the clone URL has credentials, sanitize it and store the credentials in
// a temporary file that git will access.
if strings.Contains(from, "://") && strings.Contains(from, "@") {
sanitizedFrom = util.SanitizeCredentialURLs(from)
if parsedFromURL != nil {
if pwd, has := parsedFromURL.User.Password(); has {
parsedFromURL.User = url.User(parsedFromURL.User.Username())
fromURL = parsedFromURL.String()
credentialsFile, err := os.CreateTemp(os.TempDir(), "forgejo-clone-credentials")
if err != nil {
return err
}
credentialsPath := credentialsFile.Name()
defer func() {
_ = credentialsFile.Close()
if err := util.Remove(credentialsPath); err != nil {
log.Warn("Unable to remove temporary file %q: %v", credentialsPath, err)
}
}()
// Make it read-write.
if err := credentialsFile.Chmod(0o600); err != nil {
return err
}
// Write the password.
if _, err := fmt.Fprint(credentialsFile, pwd); err != nil {
return err
}
askpassFile, err := os.CreateTemp(os.TempDir(), "forgejo-askpass")
if err != nil {
return err
}
askpassPath := askpassFile.Name()
defer func() {
_ = askpassFile.Close()
if err := util.Remove(askpassPath); err != nil {
log.Warn("Unable to remove temporary file %q: %v", askpassPath, err)
}
}()
// Make it executable.
if err := askpassFile.Chmod(0o700); err != nil {
return err
}
// Write the password script.
if _, err := fmt.Fprintf(askpassFile, "exec cat %s", credentialsPath); err != nil {
return err
}
// Close it, so that Git can use it and no busy errors arise.
_ = askpassFile.Close()
_ = credentialsFile.Close()
// Use environments to specify that git should ask for credentials, this
// takes precedences over anything else https://git-scm.com/docs/gitcredentials#_requesting_credentials.
envs = append(envs, "GIT_ASKPASS="+askpassPath)
}
}
}
cmd.SetDescription(fmt.Sprintf("clone branch %s from %s to %s (shared: %t, mirror: %t, depth: %d)", opts.Branch, sanitizedFrom, to, opts.Shared, opts.Mirror, opts.Depth))
cmd.AddDashesAndList(fromURL, to)
if opts.Timeout <= 0 { if opts.Timeout <= 0 {
opts.Timeout = -1 opts.Timeout = -1
} }
envs := os.Environ()
u, err := url.Parse(from)
if err == nil {
envs = proxy.EnvWithProxy(u)
}
stderr := new(bytes.Buffer) stderr := new(bytes.Buffer)
if err = cmd.Run(&RunOpts{ if err = cmd.Run(&RunOpts{
Timeout: opts.Timeout, Timeout: opts.Timeout,