Commit 0c7b382159

0c7b382159eb8ab7f8c6840626d0fce3fa15eb13

parent: fad7a633f4

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-29 02:27 UTC

control: mirror add refuses a numerically written host

Ref #298

Layout: unified · split

CHANGELOG.org +3
@@ -19,6 +19,9 @@ anything beyond "replace the binary and restart" is needed.
19 global gitconfig are ignored. A source that redirects (a renamed 19 global gitconfig are ignored. A source that redirects (a renamed
20 repository) fails; import from the URL it redirects to. Needs git 20 repository) fails; import from the URL it redirects to. Needs git
21 2.37 or later on the server (#298). 21 2.37 or later on the server (#298).
22- =repo mirror add= refuses a host written numerically (=127.1=,
23 =2130706433=, =0x7f.1=) when the mirror is added, rather than at its
24 first sync (#298).
22- A browser session creates credentials and grants access — keys, PGP 25- A browser session creates credentials and grants access — keys, PGP
23 keys, tokens, verified addresses, org and repository roles, transfers, 26 keys, tokens, verified addresses, org and repository roles, transfers,
24 webhooks, secrets, mirrors, and the admin promote/enable actions — 27 webhooks, secrets, mirrors, and the admin promote/enable actions —
internal/control/mirrorcmd.go +9
@@ -5,9 +5,11 @@ import (
5 "errors" 5 "errors"
6 "fmt" 6 "fmt"
7 "io" 7 "io"
8 "net/url"
8 "strconv" 9 "strconv"
9 "strings" 10 "strings"
10 11
12 "gitbay.org/gitbay/internal/gitpin"
11 "gitbay.org/gitbay/internal/policy" 13 "gitbay.org/gitbay/internal/policy"
12 "gitbay.org/gitbay/internal/protocol" 14 "gitbay.org/gitbay/internal/protocol"
13 "gitbay.org/gitbay/internal/store" 15 "gitbay.org/gitbay/internal/store"
@@ -54,6 +56,13 @@ func runMirrorAdd(c *Ctx, args []string) int {
54 if path == "" || urlArg == "" || (direction != "push" && direction != "pull") { 56 if path == "" || urlArg == "" || (direction != "push" && direction != "pull") {
55 return c.usage() 57 return c.usage()
56 } 58 }
59 // Sync refuses a numerically written host; say so now, before a
60 // resolver gets to read it.
61 if u, err := url.Parse(urlArg); err == nil {
62 if err := gitpin.CheckHost(u.Hostname()); err != nil {
63 return c.failInput(err)
64 }
65 }
57 // The worker's git process dials this URL from the server: same SSRF 66 // The worker's git process dials this URL from the server: same SSRF
58 // surface as a webhook target, same rules. 67 // surface as a webhook target, same rules.
59 if err := webhook.ValidateURL(urlArg, c.Cfg.Webhooks.AllowLocal); err != nil { 68 if err := webhook.ValidateURL(urlArg, c.Cfg.Webhooks.AllowLocal); err != nil {
internal/control/mirrorcmd_test.go added +29
@@ -0,0 +1,29 @@
1package control
2
3import (
4 "strings"
5 "testing"
6
7 "gitbay.org/gitbay/internal/protocol"
8)
9
10// mirror add refuses a host sync would refuse as numeric, on an
11// instance that allows local targets or not, and stores nothing (#298).
12func TestMirrorAddRefusesNumericHost(t *testing.T) {
13 for _, allowLocal := range []bool{true, false} {
14 for _, host := range []string{"127.1", "2130706433", "0x7f.1"} {
15 c, errOut, st, _ := importCtx(t, allowLocal)
16 code := Dispatch(c, []string{"repo", "mirror", "add", "alice/app", "https://" + host + "/x.git", "--direction", "pull"})
17 if code != protocol.ExitUsage || !strings.Contains(errOut.String(), "numeric address") {
18 t.Fatalf("%s (allow_local %v): exit %d, %q", host, allowLocal, code, errOut.String())
19 }
20 repo, err := st.RepoByPath("alice/app")
21 if err != nil {
22 t.Fatal(err)
23 }
24 if ms, err := st.ListMirrors(repo.ID); err != nil || len(ms) != 0 {
25 t.Fatalf("%s: mirrors %v, %v", host, ms, err)
26 }
27 }
28 }
29}
internal/gitpin/gitpin.go +13 −4
@@ -46,10 +46,8 @@ func Resolve(ctx context.Context, lookup Lookup, raw string, allowLocal bool) (R
46 if host == "" { 46 if host == "" {
47 return Remote{}, fmt.Errorf("URL has no host") 47 return Remote{}, fmt.Errorf("URL has no host")
48 } 48 }
49 if net.ParseIP(host) == nil && numericHost(host) { 49 if err := CheckHost(host); err != nil {
50 // 127.1, 2130706433 and 0x7f.1 are loopback to curl's parser 50 return Remote{}, err
51 // but not to Go's; refuse rather than leave them to a resolver.
52 return Remote{}, fmt.Errorf("host %q is a numeric address in a form other than dotted decimal; write it as a.b.c.d", host)
53 } 51 }
54 ips, err := lookup(ctx, host) 52 ips, err := lookup(ctx, host)
55 if err != nil { 53 if err != nil {
@@ -65,6 +63,17 @@ func Resolve(ctx context.Context, lookup Lookup, raw string, allowLocal bool) (R
65 return Remote{URL: u, IPs: ips}, nil 63 return Remote{URL: u, IPs: ips}, nil
66} 64}
67 65
66// CheckHost refuses a host written as a number in a form other than
67// an IP literal: 127.1, 2130706433 and 0x7f.1 are loopback to curl's
68// parser but not to Go's, so they are refused rather than left to a
69// resolver.
70func CheckHost(host string) error {
71 if net.ParseIP(host) == nil && numericHost(host) {
72 return fmt.Errorf("host %q is a numeric address in a form other than dotted decimal; write it as a.b.c.d", host)
73 }
74 return nil
75}
76
68// numericHost reports whether every label of host is a decimal, octal 77// numericHost reports whether every label of host is a decimal, octal
69// or hex number, the shapes inet_aton reads as an IPv4 address. 78// or hex number, the shapes inet_aton reads as an IPv4 address.
70func numericHost(host string) bool { 79func numericHost(host string) bool {