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 {
93 // land in process listings and logs. 93 // land in process listings and logs.
94 return c.fail(protocol.ExitUsage, "do not embed credentials in the URL; use --token-stdin") 94 return c.fail(protocol.ExitUsage, "do not embed credentials in the URL; use --token-stdin")
95 } 95 }
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 }
96 100
97 // Resolve and check the host now and hold git to those addresses, 101 // Resolve and check the host now and hold git to those addresses,
98 // as mirror sync does (#298). 102 // as mirror sync does (#298).
@@ -160,7 +164,7 @@ func runRepoImport(c *Ctx, args []string) int {
160 return c.fail(protocol.ExitFailure, "import failed: %v", err) 164 return c.fail(protocol.ExitFailure, "import failed: %v", err)
161 } 165 }
162 166
163 branch, err := gitutil.RemoteDefaultBranch(ctx, from, remote.Args(), env) 167 branch, err := gitutil.RemoteDefaultBranch(ctx, dir, from, remote.Args(), env)
164 if err != nil { 168 if err != nil {
165 branch = "main" // remote gone quiet after the fetch; keep the default 169 branch = "main" // remote gone quiet after the fetch; keep the default
166 } 170 }
internal/control/import_test.go +41 −4
@@ -64,12 +64,13 @@ func TestRepoImportRefusesGitScheme(t *testing.T) {
64 } 64 }
65} 65}
66 66
67// A source on loopback is refused on a default instance, and nothing is 67// A reachable source on loopback is refused on a default instance, and
68// left behind. 68// nothing is left behind.
69func TestRepoImportRefusesALocalAddress(t *testing.T) { 69func TestRepoImportRefusesALocalAddress(t *testing.T) {
70 remote, _ := importUpstream(t)
70 c, errOut, st, root := importCtx(t, false) 71 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 code := Dispatch(c, []string{"repo", "import", "alice/x", "--from", remote})
72 if code != protocol.ExitFailure || !strings.Contains(errOut.String(), "127.0.0.1") { 73 if code != protocol.ExitFailure || !strings.Contains(errOut.String(), "private or local address") {
73 t.Fatalf("exit %d, %q", code, errOut.String()) 74 t.Fatalf("exit %d, %q", code, errOut.String())
74 } 75 }
75 if _, err := st.RepoByPath("alice/x"); err == nil { 76 if _, err := st.RepoByPath("alice/x"); err == nil {
@@ -80,11 +81,47 @@ func TestRepoImportRefusesALocalAddress(t *testing.T) {
80 } 81 }
81} 82}
82 83
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
83// import.test does not resolve; the import works only because git was 99// import.test does not resolve; the import works only because git was
84// pinned to the address import looked up and checked. 100// pinned to the address import looked up and checked.
85func TestRepoImportConnectsToTheCheckedAddress(t *testing.T) { 101func 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()
86 remote, sha := importUpstream(t) 122 remote, sha := importUpstream(t)
87 u, _ := url.Parse(remote) 123 u, _ := url.Parse(remote)
124 setup(u.Port())
88 c, errOut, _, root := importCtx(t, true) 125 c, errOut, _, root := importCtx(t, true)
89 var asked []string 126 var asked []string
90 prev := importLookup 127 prev := importLookup
internal/gitutil/gitutil.go +4 −4
@@ -255,10 +255,10 @@ func FetchPullHeads(ctx context.Context, dir, url string, errW io.Writer, extraE
255 return nil 255 return nil
256} 256}
257 257
258// RemoteDefaultBranch asks the remote which branch HEAD points at. pin 258// RemoteDefaultBranch asks the remote which branch HEAD points at,
259// and env are as for FetchMirror. 259// running in the repository at dir. pin and env are as for FetchMirror.
260func RemoteDefaultBranch(ctx context.Context, url string, pin, env []string) (string, error) { 260func RemoteDefaultBranch(ctx context.Context, dir, url string, pin, env []string) (string, error) {
261 args := append(append([]string{}, pin...), "ls-remote", "--symref", url, "HEAD") 261 args := append(append([]string{}, pin...), "-C", dir, "ls-remote", "--symref", url, "HEAD")
262 cmd := exec.CommandContext(ctx, toolpath.Look("git"), args...) 262 cmd := exec.CommandContext(ctx, toolpath.Look("git"), args...)
263 cmd.Env = env 263 cmd.Env = env
264 out, err := cmd.Output() 264 out, err := cmd.Output()