mirror of https://github.com/gogs/gogs.git
security: fix SSRF in repository migration (#6812)
Co-authored-by: Joe Chen <jc@unknwon.io>pull/6819/merge
parent
bb19f52c05
commit
242deca524
|
@ -21,6 +21,7 @@ All notable changes to Gogs are documented in this file.
|
||||||
|
|
||||||
### Fixed
|
### 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)
|
- Unable to use LDAP authentication on ARM machines. [#6761](https://github.com/gogs/gogs/issues/6761)
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"gopkg.in/macaron.v1"
|
"gopkg.in/macaron.v1"
|
||||||
|
|
||||||
"gogs.io/gogs/internal/db"
|
"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 {
|
if err != nil {
|
||||||
return "", db.ErrInvalidCloneAddr{IsURLError: true}
|
return "", db.ErrInvalidCloneAddr{IsURLError: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if netutil.IsLocalHostname(u.Hostname()) {
|
||||||
|
return "", db.ErrInvalidCloneAddr{IsURLError: true}
|
||||||
|
}
|
||||||
|
|
||||||
if len(f.AuthUsername)+len(f.AuthPassword) > 0 {
|
if len(f.AuthUsername)+len(f.AuthPassword) > 0 {
|
||||||
u.User = url.UserPassword(f.AuthUsername, f.AuthPassword)
|
u.User = url.UserPassword(f.AuthUsername, f.AuthPassword)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"gogs.io/gogs/internal/db"
|
"gogs.io/gogs/internal/db"
|
||||||
"gogs.io/gogs/internal/db/errors"
|
"gogs.io/gogs/internal/db/errors"
|
||||||
"gogs.io/gogs/internal/form"
|
"gogs.io/gogs/internal/form"
|
||||||
|
"gogs.io/gogs/internal/netutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -118,23 +119,6 @@ func WebhooksNew(c *context.Context, orCtx *orgRepoContext) {
|
||||||
c.Success(orCtx.TmplNew)
|
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) {
|
func validateWebhook(actor *db.User, l macaron.Locale, w *db.Webhook) (field, msg string, ok bool) {
|
||||||
if !actor.IsAdmin {
|
if !actor.IsAdmin {
|
||||||
// 🚨 SECURITY: Local addresses must not be allowed by non-admins to prevent SSRF,
|
// 🚨 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
|
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
|
return "PayloadURL", l.Tr("repo.settings.webhook.err_cannot_use_local_addresses"), false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,25 +13,6 @@ import (
|
||||||
"gogs.io/gogs/internal/mocks"
|
"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) {
|
func Test_validateWebhook(t *testing.T) {
|
||||||
l := &mocks.Locale{
|
l := &mocks.Locale{
|
||||||
MockLang: "en",
|
MockLang: "en",
|
||||||
|
|
Loading…
Reference in New Issue