From f7f7d086e4950fc6bd80450cd64623588de07dd3 Mon Sep 17 00:00:00 2001 From: Gusted Date: Sun, 24 Aug 2025 02:30:23 +0200 Subject: [PATCH] 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. --- modules/git/repo.go | 88 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 11 deletions(-) diff --git a/modules/git/repo.go b/modules/git/repo.go index 23e9337615..4f2b05bca5 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -18,6 +18,7 @@ import ( "strings" "time" + "forgejo.org/modules/log" "forgejo.org/modules/proxy" "forgejo.org/modules/setting" "forgejo.org/modules/util" @@ -160,24 +161,89 @@ func CloneWithArgs(ctx context.Context, args TrustedCmdArgs, from, to string, op if len(opts.Branch) > 0 { cmd.AddArguments("-b").AddDynamicArguments(opts.Branch) } - cmd.AddDashesAndList(from, to) - if strings.Contains(from, "://") && strings.Contains(from, "@") { - 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)) - } else { - 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 := os.Environ() + parsedFromURL, err := url.Parse(from) + if err == nil { + 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 { opts.Timeout = -1 } - envs := os.Environ() - u, err := url.Parse(from) - if err == nil { - envs = proxy.EnvWithProxy(u) - } - stderr := new(bytes.Buffer) if err = cmd.Run(&RunOpts{ Timeout: opts.Timeout,