Skip to content

Commit

Permalink
security: fix SSRF in repository migration (#6812)
Browse files Browse the repository at this point in the history
Co-authored-by: Joe Chen <jc@unknwon.io>
# Conflicts:
#	CHANGELOG.md
#	internal/route/repo/webhook.go
  • Loading branch information
michaellrowley authored and unknwon committed Mar 11, 2022
1 parent b354103 commit 91f2cde
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 38 deletions.
6 changes: 6 additions & 0 deletions internal/form/repo.go
Expand Up @@ -13,6 +13,7 @@ import (
"gopkg.in/macaron.v1"

"gogs.io/gogs/internal/db"
"gogs.io/gogs/internal/netutil"
)

// _______________________________________ _________.______________________ _______________.___.
Expand Down Expand Up @@ -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)
}
Expand Down
64 changes: 64 additions & 0 deletions 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 changes: 36 additions & 0 deletions 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))
})
}
}
22 changes: 3 additions & 19 deletions internal/route/repo/webhook.go
Expand Up @@ -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 (
Expand Down Expand Up @@ -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.
Expand All @@ -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
}
}
Expand Down
19 changes: 0 additions & 19 deletions internal/route/repo/webhook_test.go
Expand Up @@ -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",
Expand Down

0 comments on commit 91f2cde

Please sign in to comment.