diff --git a/internal/form/repo.go b/internal/form/repo.go index 981aa31a1a..1e9082d879 100644 --- a/internal/form/repo.go +++ b/internal/form/repo.go @@ -13,6 +13,7 @@ import ( "gopkg.in/macaron.v1" "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/netutil" ) // _______________________________________ _________.______________________ _______________.___. @@ -67,6 +68,11 @@ func (f MigrateRepo) ParseRemoteAddr(user *db.User) (string, error) { if err != nil { return "", db.ErrInvalidCloneAddr{IsURLError: true} } + + if netutil.IsLocalHostname(u.Hostname()) { + return "", db.ErrInvalidCloneAddr{IsURLError: true} + } + if len(f.AuthUsername)+len(f.AuthPassword) > 0 { u.User = url.UserPassword(f.AuthUsername, f.AuthPassword) } diff --git a/internal/netutil/netutil.go b/internal/netutil/netutil.go new file mode 100644 index 0000000000..e3b3b8cc5c --- /dev/null +++ b/internal/netutil/netutil.go @@ -0,0 +1,64 @@ +// Copyright 2022 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package netutil + +import ( + "fmt" + "net" +) + +var localCIDRs []*net.IPNet + +func init() { + // Parsing hardcoded CIDR strings should never fail, if in case it does, let's + // fail it at start. + rawCIDRs := []string{ + // https://datatracker.ietf.org/doc/html/rfc5735: + "127.0.0.0/8", // Loopback + "0.0.0.0/8", // "This" network + "100.64.0.0/10", // Shared address space + "169.254.0.0/16", // Link local + "172.16.0.0/12", // Private-use networks + "192.0.0.0/24", // IETF Protocol assignments + "192.0.2.0/24", // TEST-NET-1 + "192.88.99.0/24", // 6to4 Relay anycast + "192.168.0.0/16", // Private-use networks + "198.18.0.0/15", // Network interconnect + "198.51.100.0/24", // TEST-NET-2 + "203.0.113.0/24", // TEST-NET-3 + "255.255.255.255/32", // Limited broadcast + + // https://datatracker.ietf.org/doc/html/rfc1918: + "10.0.0.0/8", // Private-use networks + + // https://datatracker.ietf.org/doc/html/rfc6890: + "::1/128", // Loopback + "FC00::/7", // Unique local address + "FE80::/10", // Multicast address + } + for _, raw := range rawCIDRs { + _, cidr, err := net.ParseCIDR(raw) + if err != nil { + panic(fmt.Sprintf("parse CIDR %q: %v", raw, err)) + } + localCIDRs = append(localCIDRs, cidr) + } +} + +// IsLocalHostname returns true if given hostname is a known local address. +func IsLocalHostname(hostname string) bool { + ips, err := net.LookupIP(hostname) + if err != nil { + return true + } + for _, ip := range ips { + for _, cidr := range localCIDRs { + if cidr.Contains(ip) { + return true + } + } + } + return false +} diff --git a/internal/netutil/netutil_test.go b/internal/netutil/netutil_test.go new file mode 100644 index 0000000000..47be4e74b4 --- /dev/null +++ b/internal/netutil/netutil_test.go @@ -0,0 +1,36 @@ +// Copyright 2022 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package netutil + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsLocalHostname(t *testing.T) { + tests := []struct { + hostname string + want bool + }{ + {hostname: "localhost", want: true}, + {hostname: "127.0.0.1", want: true}, + {hostname: "::1", want: true}, + {hostname: "0:0:0:0:0:0:0:1", want: true}, + {hostname: "fuf.me", want: true}, + {hostname: "127.0.0.95", want: true}, + {hostname: "0.0.0.0", want: true}, + {hostname: "192.168.123.45", want: true}, + + {hostname: "gogs.io", want: false}, + {hostname: "google.com", want: false}, + {hostname: "165.232.140.255", want: false}, + } + for _, test := range tests { + t.Run("", func(t *testing.T) { + assert.Equal(t, test.want, IsLocalHostname(test.hostname)) + }) + } +} diff --git a/internal/route/repo/webhook.go b/internal/route/repo/webhook.go index 8e4b3a11b7..77696bbcd0 100644 --- a/internal/route/repo/webhook.go +++ b/internal/route/repo/webhook.go @@ -20,6 +20,7 @@ import ( "gogs.io/gogs/internal/db" "gogs.io/gogs/internal/db/errors" "gogs.io/gogs/internal/form" + "gogs.io/gogs/internal/netutil" ) const ( @@ -118,24 +119,7 @@ func WebhooksNew(c *context.Context, orCtx *orgRepoContext) { c.Success(orCtx.TmplNew) } -var localHostnames = []string{ - "localhost", - "127.0.0.1", - "::1", - "0:0:0:0:0:0:0:1", -} - -// isLocalHostname returns true if given hostname is a known local address. -func isLocalHostname(hostname string) bool { - for _, local := range localHostnames { - if hostname == local { - return true - } - } - return false -} - -func validateWebhook(actor *db.User, l macaron.Locale, w *db.Webhook) (field string, msg string, ok bool) { +func validateWebhook(actor *db.User, l macaron.Locale, w *db.Webhook) (field, msg string, ok bool) { if !actor.IsAdmin { // 🚨 SECURITY: Local addresses must not be allowed by non-admins to prevent SSRF, // see https://github.com/gogs/gogs/issues/5366 for details. @@ -144,7 +128,7 @@ func validateWebhook(actor *db.User, l macaron.Locale, w *db.Webhook) (field str return "PayloadURL", l.Tr("repo.settings.webhook.err_cannot_parse_payload_url", err), false } - if isLocalHostname(payloadURL.Hostname()) { + if netutil.IsLocalHostname(payloadURL.Hostname()) { return "PayloadURL", l.Tr("repo.settings.webhook.err_cannot_use_local_addresses"), false } } diff --git a/internal/route/repo/webhook_test.go b/internal/route/repo/webhook_test.go index 182c6eed7e..d10a6fcc0c 100644 --- a/internal/route/repo/webhook_test.go +++ b/internal/route/repo/webhook_test.go @@ -13,25 +13,6 @@ import ( "gogs.io/gogs/internal/mocks" ) -func Test_isLocalHostname(t *testing.T) { - tests := []struct { - hostname string - want bool - }{ - {hostname: "localhost", want: true}, - {hostname: "127.0.0.1", want: true}, - {hostname: "::1", want: true}, - {hostname: "0:0:0:0:0:0:0:1", want: true}, - - {hostname: "gogs.io", want: false}, - } - for _, test := range tests { - t.Run("", func(t *testing.T) { - assert.Equal(t, test.want, isLocalHostname(test.hostname)) - }) - } -} - func Test_validateWebhook(t *testing.T) { l := &mocks.Locale{ MockLang: "en",