security: fix SSRF in repository migration (#6812)

Co-authored-by: Joe Chen <jc@unknwon.io>
pull/6819/merge
Michael Rowley 2022-03-08 03:34:53 +00:00 committed by GitHub
parent bb19f52c05
commit 242deca524
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 109 additions and 37 deletions

View File

@ -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

View File

@ -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)
}

View File

@ -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
}

View File

@ -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))
})
}
}

View File

@ -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
}
}

View File

@ -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",