Browse Source

security: fix SSRF in repository migration (#6812)

Co-authored-by: Joe Chen <[email protected]>
Michael Rowley 3 years ago
parent
commit
242deca524

+ 1 - 0
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

+ 6 - 0
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)
 		}

+ 64 - 0
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
+}

+ 36 - 0
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))
+		})
+	}
+}

+ 2 - 18
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
 		}
 	}

+ 0 - 19
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",