diff options
author | Michael Rowley <michaellrowley@protonmail.com> | 2022-03-08 03:34:53 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-03-08 11:34:53 +0800 |
commit | 242deca524dbf922bfb08dadd65455164b9e663e (patch) | |
tree | b8110c947dba99cf3e8115219a440f79f19bcc14 | |
parent | bb19f52c05e212b9358f9efaa897120dbdf9d0ab (diff) |
security: fix SSRF in repository migration (#6812)
Co-authored-by: Joe Chen <jc@unknwon.io>
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | internal/form/repo.go | 6 | ||||
-rw-r--r-- | internal/netutil/netutil.go | 64 | ||||
-rw-r--r-- | internal/netutil/netutil_test.go | 36 | ||||
-rw-r--r-- | internal/route/repo/webhook.go | 20 | ||||
-rw-r--r-- | internal/route/repo/webhook_test.go | 19 |
6 files changed, 109 insertions, 37 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index cbb9eff3..1a1a4def 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ All notable changes to Gogs are documented in this file. ### Fixed +- _Security:_ Potential SSRF in repository migration. [#6754](https://github.com/gogs/gogs/issues/6754) - Unable to use LDAP authentication on ARM machines. [#6761](https://github.com/gogs/gogs/issues/6761) ### Removed diff --git a/internal/form/repo.go b/internal/form/repo.go index ed963307..bc0dc426 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" ) // _______________________________________ _________.______________________ _______________.___. @@ -69,6 +70,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 00000000..e3b3b8cc --- /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 00000000..47be4e74 --- /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 43148822..77696bbc 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,23 +119,6 @@ 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, msg string, ok bool) { if !actor.IsAdmin { // 🚨 SECURITY: Local addresses must not be allowed by non-admins to prevent SSRF, @@ -144,7 +128,7 @@ func validateWebhook(actor *db.User, l macaron.Locale, w *db.Webhook) (field, ms 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 182c6eed..d10a6fcc 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", |