Commit ecfc702093

ecfc70209326eea82c339a536b546c4a560ac0b3

parent: c32afe91e2

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-29 00:23 UTC

import: refuse a query or fragment, ls-remote in the new repository

Ref #298

Layout: unified · split

internal/control/import.go +5 −1
@@ -93,6 +93,10 @@ func runRepoImport(c *Ctx, args []string) int {
9393 // land in process listings and logs.
9494 return c.fail(protocol.ExitUsage, "do not embed credentials in the URL; use --token-stdin")
9595 }
96 if strings.ContainsAny(from, "?#") {
97 // The URL is logged and recorded; a query could carry a token.
98 return c.fail(protocol.ExitUsage, "use the plain clone URL, without a query or fragment; credentials go on stdin with --token-stdin, never in the URL")
99 }
96100
97101 // Resolve and check the host now and hold git to those addresses,
98102 // as mirror sync does (#298).
@@ -160,7 +164,7 @@ func runRepoImport(c *Ctx, args []string) int {
160164 return c.fail(protocol.ExitFailure, "import failed: %v", err)
161165 }
162166
163 branch, err := gitutil.RemoteDefaultBranch(ctx, from, remote.Args(), env)
167 branch, err := gitutil.RemoteDefaultBranch(ctx, dir, from, remote.Args(), env)
164168 if err != nil {
165169 branch = "main" // remote gone quiet after the fetch; keep the default
166170 }
internal/control/import_test.go +41 −4
@@ -64,12 +64,13 @@ func TestRepoImportRefusesGitScheme(t *testing.T) {
6464 }
6565}
6666
67// A source on loopback is refused on a default instance, and nothing is
68// left behind.
67// A reachable source on loopback is refused on a default instance, and
68// nothing is left behind.
6969func TestRepoImportRefusesALocalAddress(t *testing.T) {
70 remote, _ := importUpstream(t)
7071 c, errOut, st, root := importCtx(t, false)
71 code := Dispatch(c, []string{"repo", "import", "alice/x", "--from", "http://127.0.0.1:9/x.git"})
72 if code != protocol.ExitFailure || !strings.Contains(errOut.String(), "127.0.0.1") {
72 code := Dispatch(c, []string{"repo", "import", "alice/x", "--from", remote})
73 if code != protocol.ExitFailure || !strings.Contains(errOut.String(), "private or local address") {
7374 t.Fatalf("exit %d, %q", code, errOut.String())
7475 }
7576 if _, err := st.RepoByPath("alice/x"); err == nil {
@@ -80,11 +81,47 @@ func TestRepoImportRefusesALocalAddress(t *testing.T) {
8081 }
8182}
8283
84// A query or fragment could carry a credential into the log and the
85// event row; import refuses it before either.
86func TestRepoImportRefusesAQuery(t *testing.T) {
87 for _, from := range []string{"https://git.example/x.git?token=abc", "https://git.example/x.git#abc"} {
88 c, errOut, st, _ := importCtx(t, true)
89 code := Dispatch(c, []string{"repo", "import", "alice/x", "--from", from})
90 if code != protocol.ExitUsage || !strings.Contains(errOut.String(), "plain clone URL") || strings.Contains(errOut.String(), "abc") {
91 t.Fatalf("%s: exit %d, %q", from, code, errOut.String())
92 }
93 if _, err := st.RepoByPath("alice/x"); err == nil {
94 t.Fatalf("%s: a refused import created a repository", from)
95 }
96 }
97}
98
8399// import.test does not resolve; the import works only because git was
84100// pinned to the address import looked up and checked.
85101func TestRepoImportConnectsToTheCheckedAddress(t *testing.T) {
102 importThroughPin(t, func(string) {})
103}
104
105// git runs with its own environment, not the daemon's: a proxy or a
106// global URL rewrite there would take it around the pin.
107func TestRepoImportIgnoresTheDaemonEnvironment(t *testing.T) {
108 importThroughPin(t, func(port string) {
109 t.Setenv("http_proxy", "http://127.0.0.1:1")
110 t.Setenv("HTTP_PROXY", "http://127.0.0.1:1")
111 global := filepath.Join(t.TempDir(), "gitconfig")
112 rewrite := "[url \"http://127.0.0.1:1/\"]\n\tinsteadOf = http://import.test:" + port + "/\n"
113 if err := os.WriteFile(global, []byte(rewrite), 0o644); err != nil {
114 t.Fatal(err)
115 }
116 t.Setenv("GIT_CONFIG_GLOBAL", global)
117 })
118}
119
120func importThroughPin(t *testing.T, setup func(port string)) {
121 t.Helper()
86122 remote, sha := importUpstream(t)
87123 u, _ := url.Parse(remote)
124 setup(u.Port())
88125 c, errOut, _, root := importCtx(t, true)
89126 var asked []string
90127 prev := importLookup
internal/gitutil/gitutil.go +4 −4
@@ -255,10 +255,10 @@ func FetchPullHeads(ctx context.Context, dir, url string, errW io.Writer, extraE
255255 return nil
256256}
257257
258// RemoteDefaultBranch asks the remote which branch HEAD points at. pin
259// and env are as for FetchMirror.
260func RemoteDefaultBranch(ctx context.Context, url string, pin, env []string) (string, error) {
261 args := append(append([]string{}, pin...), "ls-remote", "--symref", url, "HEAD")
258// RemoteDefaultBranch asks the remote which branch HEAD points at,
259// running in the repository at dir. pin and env are as for FetchMirror.
260func RemoteDefaultBranch(ctx context.Context, dir, url string, pin, env []string) (string, error) {
261 args := append(append([]string{}, pin...), "-C", dir, "ls-remote", "--symref", url, "HEAD")
262262 cmd := exec.CommandContext(ctx, toolpath.Look("git"), args...)
263263 cmd.Env = env
264264 out, err := cmd.Output()