Commit c32afe91e2

c32afe91e21cc75d0dff46452074510659b51ac0

parent: 1c6440454b

Verified · cmc

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

gitpin: refuse odd numeric hosts, pin every name on the port

Ref #298

Layout: unified · split

internal/gitpin/gitpin.go +27 −2
@@ -46,6 +46,11 @@ func Resolve(ctx context.Context, lookup Lookup, raw string, allowLocal bool) (R
4646 if host == "" {
4747 return Remote{}, fmt.Errorf("URL has no host")
4848 }
49 if net.ParseIP(host) == nil && numericHost(host) {
50 // 127.1, 2130706433 and 0x7f.1 are loopback to curl's parser
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 }
4954 ips, err := lookup(ctx, host)
5055 if err != nil {
5156 return Remote{}, fmt.Errorf("resolving %s: %w", host, err)
@@ -60,8 +65,24 @@ func Resolve(ctx context.Context, lookup Lookup, raw string, allowLocal bool) (R
6065 return Remote{URL: u, IPs: ips}, nil
6166}
6267
68// numericHost reports whether every label of host is a decimal, octal
69// or hex number, the shapes inet_aton reads as an IPv4 address.
70func numericHost(host string) bool {
71 for _, label := range strings.Split(strings.TrimSuffix(host, "."), ".") {
72 digits, base := label, "0123456789"
73 if rest, ok := strings.CutPrefix(strings.ToLower(label), "0x"); ok {
74 digits, base = rest, "0123456789abcdef"
75 }
76 if strings.Trim(strings.ToLower(digits), base) != "" || label == "" {
77 return false
78 }
79 }
80 return true
81}
82
6383// Args are git's leading -c options for r: curl's resolve list pins
64// the host to the checked addresses, and with redirects off a server
84// the host, and any other name on the same port, to the checked
85// addresses, and with redirects off a server
6586// cannot send git on to a host nobody checked. An address literal
6687// needs no pin.
6788func (r Remote) Args() []string {
@@ -85,7 +106,11 @@ func (r Remote) Args() []string {
85106 addrs[i] = ip.String()
86107 }
87108 }
88 return append(args, "-c", "http.curloptResolve="+host+":"+port+":"+strings.Join(addrs, ","))
109 pinned := port + ":" + strings.Join(addrs, ",")
110 // The wildcard entry catches a lookup under any other spelling of
111 // the host, so it too lands on the checked addresses.
112 return append(args, "-c", "http.curloptResolve="+host+":"+pinned,
113 "-c", "http.curloptResolve=*:"+pinned)
89114}
90115
91116// Env is git's whole environment for a pinned remote. No system or
internal/gitpin/gitpin_test.go +24 −2
@@ -46,16 +46,38 @@ func TestResolve(t *testing.T) {
4646 }
4747}
4848
49// Numeric hosts other than a dotted quad are refused before any lookup:
50// curl reads them as addresses the check never saw.
51func TestResolveRefusesOddNumericHosts(t *testing.T) {
52 never := func(_ context.Context, host string) ([]net.IP, error) {
53 t.Fatalf("looked up %s", host)
54 return nil, nil
55 }
56 for _, host := range []string{"127.1", "2130706433", "0x7f.1", "0x7F000001", "017700000001", "127.0.0.01", "127.0.0.1."} {
57 if _, err := Resolve(context.Background(), never, "http://"+host+"/x.git", true); err == nil || !strings.Contains(err.Error(), "numeric address") {
58 t.Errorf("%s: %v", host, err)
59 }
60 }
61 // Names with a numeric label, and real literals, still pass.
62 for _, host := range []string{"1.example", "0x7f.example", "203.0.113.5", "[2001:db8::1]"} {
63 if _, err := Resolve(context.Background(), answer("203.0.113.5"), "http://"+host+"/x.git", false); err != nil {
64 t.Errorf("%s: %v", host, err)
65 }
66 }
67}
68
4969func TestArgs(t *testing.T) {
5070 u, _ := url.Parse("https://git.example/x.git")
5171 got := Remote{u, []net.IP{net.ParseIP("203.0.113.5"), net.ParseIP("2001:db8::1")}}.Args()
5272 want := []string{"-c", "http.followRedirects=false",
53 "-c", "http.curloptResolve=git.example:443:203.0.113.5,[2001:db8::1]"}
73 "-c", "http.curloptResolve=git.example:443:203.0.113.5,[2001:db8::1]",
74 "-c", "http.curloptResolve=*:443:203.0.113.5,[2001:db8::1]"}
5475 if !slices.Equal(got, want) {
5576 t.Fatalf("https: %q", got)
5677 }
5778 u, _ = url.Parse("http://git.example:8080/x.git")
58 if got := (Remote{u, []net.IP{net.ParseIP("203.0.113.5")}}).Args(); got[3] != "http.curloptResolve=git.example:8080:203.0.113.5" {
79 if got := (Remote{u, []net.IP{net.ParseIP("203.0.113.5")}}).Args(); got[3] != "http.curloptResolve=git.example:8080:203.0.113.5" ||
80 got[5] != "http.curloptResolve=*:8080:203.0.113.5" {
5981 t.Fatalf("http with port: %q", got)
6082 }
6183 // An address literal is its own resolution; there is nothing to pin.