Skip to content

webhook: revalidate local hostname before each delivery #6988

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 31, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ All notable changes to Gogs are documented in this file.

### Fixed

- _Security:_ SSRF in webhook. [#6901](https://github.com/gogs/gogs/issues/6901)
- Unable to use LDAP authentication on ARM machines. [#6761](https://github.com/gogs/gogs/issues/6761)

### Removed
Expand Down
3 changes: 2 additions & 1 deletion conf/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,7 @@ migrate.clone_address_desc = This can be a HTTP/HTTPS/GIT URL.
migrate.clone_address_desc_import_local = You're also allowed to migrate a repository by local server path.
migrate.permission_denied = You are not allowed to import local repositories.
migrate.invalid_local_path = Invalid local path, it does not exist or not a directory.
migrate.clone_address_resolved_to_blocked_local_address = Clone address resolved to a local network address that is implicitly blocked.
migrate.failed = Migration failed: %v

mirror_from = mirror of
Expand Down Expand Up @@ -809,7 +810,7 @@ settings.webhook.headers = Headers
settings.webhook.payload = Payload
settings.webhook.body = Body
settings.webhook.err_cannot_parse_payload_url = Cannot parse payload URL: %v
settings.webhook.err_cannot_use_local_addresses = Non admins are not allowed to use local addresses.
settings.webhook.url_resolved_to_blocked_local_address = Payload URL resolved to a local network address that is implicitly blocked.
settings.githooks_desc = Git Hooks are powered by Git itself, you can edit files of supported hooks in the list below to perform custom operations.
settings.githook_edit_desc = If the hook is inactive, sample content will be presented. Leaving content to an empty value will disable this hook.
settings.githook_name = Hook Name
Expand Down
11 changes: 6 additions & 5 deletions internal/db/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,10 @@ func (err ErrLastOrgOwner) Error() string {
// \/ \/|__| \/ \/

type ErrInvalidCloneAddr struct {
IsURLError bool
IsInvalidPath bool
IsPermissionDenied bool
IsURLError bool
IsInvalidPath bool
IsPermissionDenied bool
IsBlockedLocalAddress bool
}

func IsErrInvalidCloneAddr(err error) bool {
Expand All @@ -205,8 +206,8 @@ func IsErrInvalidCloneAddr(err error) bool {
}

func (err ErrInvalidCloneAddr) Error() string {
return fmt.Sprintf("invalid clone address [is_url_error: %v, is_invalid_path: %v, is_permission_denied: %v]",
err.IsURLError, err.IsInvalidPath, err.IsPermissionDenied)
return fmt.Sprintf("invalid clone address [is_url_error: %v, is_invalid_path: %v, is_permission_denied: %v, is_blocked_local_address: %v]",
err.IsURLError, err.IsInvalidPath, err.IsPermissionDenied, err.IsBlockedLocalAddress)
}

type ErrUpdateTaskNotExist struct {
Expand Down
6 changes: 6 additions & 0 deletions internal/db/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/errutil"
"gogs.io/gogs/internal/httplib"
"gogs.io/gogs/internal/netutil"
"gogs.io/gogs/internal/sync"
)

Expand Down Expand Up @@ -688,6 +689,11 @@ func TestWebhook(repo *Repository, event HookEventType, p api.Payloader, webhook
}

func (t *HookTask) deliver() {
if netutil.IsBlockedLocalHostname(t.URL, conf.Security.LocalNetworkAllowlist) {
t.ResponseContent = "Payload URL resolved to a local network address that is implicitly blocked."
return
}

t.IsDelivered = true

timeout := time.Duration(conf.Webhook.DeliverTimeout) * time.Second
Expand Down
4 changes: 2 additions & 2 deletions internal/form/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ func (f MigrateRepo) ParseRemoteAddr(user *db.User) (string, error) {
return "", db.ErrInvalidCloneAddr{IsURLError: true}
}

if netutil.IsLocalHostname(u.Hostname(), conf.Security.LocalNetworkAllowlist) {
return "", db.ErrInvalidCloneAddr{IsURLError: true}
if netutil.IsBlockedLocalHostname(u.Hostname(), conf.Security.LocalNetworkAllowlist) {
return "", db.ErrInvalidCloneAddr{IsBlockedLocalAddress: true}
}

if len(f.AuthUsername)+len(f.AuthPassword) > 0 {
Expand Down
7 changes: 4 additions & 3 deletions internal/netutil/netutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ func init() {
}
}

// IsLocalHostname returns true if given hostname is resolved to local network
// address, except exempted from the allowlist.
func IsLocalHostname(hostname string, allowlist []string) bool {
// IsBlockedLocalHostname returns true if given hostname is resolved to a local
// network address that is implicitly blocked (i.e. not exempted from the
// allowlist).
func IsBlockedLocalHostname(hostname string, allowlist []string) bool {
for _, allow := range allowlist {
if hostname == allow {
return false
Expand Down
2 changes: 1 addition & 1 deletion internal/netutil/netutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func TestIsLocalHostname(t *testing.T) {
}
for _, test := range tests {
t.Run("", func(t *testing.T) {
assert.Equal(t, test.want, IsLocalHostname(test.hostname, test.allowlist))
assert.Equal(t, test.want, IsBlockedLocalHostname(test.hostname, test.allowlist))
})
}
}
2 changes: 2 additions & 0 deletions internal/route/api/v1/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ func Migrate(c *context.APIContext, f form.MigrateRepo) {
c.ErrorStatus(http.StatusUnprocessableEntity, errors.New("You are not allowed to import local repositories."))
case addrErr.IsInvalidPath:
c.ErrorStatus(http.StatusUnprocessableEntity, errors.New("Invalid local path, it does not exist or not a directory."))
case addrErr.IsBlockedLocalAddress:
c.ErrorStatus(http.StatusUnprocessableEntity, errors.New("Clone address resolved to a local network address that is implicitly blocked."))
default:
c.Error(err, "unexpected error")
}
Expand Down
4 changes: 3 additions & 1 deletion internal/route/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,13 @@ func MigratePost(c *context.Context, f form.MigrateRepo) {
addrErr := err.(db.ErrInvalidCloneAddr)
switch {
case addrErr.IsURLError:
c.RenderWithErr(c.Tr("form.url_error"), MIGRATE, &f)
c.RenderWithErr(c.Tr("repo.migrate.clone_address")+c.Tr("form.url_error"), MIGRATE, &f)
case addrErr.IsPermissionDenied:
c.RenderWithErr(c.Tr("repo.migrate.permission_denied"), MIGRATE, &f)
case addrErr.IsInvalidPath:
c.RenderWithErr(c.Tr("repo.migrate.invalid_local_path"), MIGRATE, &f)
case addrErr.IsBlockedLocalAddress:
c.RenderWithErr(c.Tr("repo.migrate.clone_address_resolved_to_blocked_local_address"), MIGRATE, &f)
default:
c.Error(err, "unexpected error")
}
Expand Down
25 changes: 11 additions & 14 deletions internal/route/repo/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,20 +119,17 @@ func WebhooksNew(c *context.Context, orCtx *orgRepoContext) {
c.Success(orCtx.TmplNew)
}

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.
payloadURL, err := url.Parse(w.URL)
if err != nil {
return "PayloadURL", l.Tr("repo.settings.webhook.err_cannot_parse_payload_url", err), false
}

if netutil.IsLocalHostname(payloadURL.Hostname(), conf.Security.LocalNetworkAllowlist) {
return "PayloadURL", l.Tr("repo.settings.webhook.err_cannot_use_local_addresses"), false
}
func validateWebhook(l macaron.Locale, w *db.Webhook) (field, msg string, ok bool) {
// 🚨 SECURITY: Local addresses must not be allowed by non-admins to prevent SSRF,
// see https://github.com/gogs/gogs/issues/5366 for details.
payloadURL, err := url.Parse(w.URL)
if err != nil {
return "PayloadURL", l.Tr("repo.settings.webhook.err_cannot_parse_payload_url", err), false
}

if netutil.IsBlockedLocalHostname(payloadURL.Hostname(), conf.Security.LocalNetworkAllowlist) {
return "PayloadURL", l.Tr("repo.settings.webhook.url_resolved_to_blocked_local_address"), false
}
return "", "", true
}

Expand All @@ -144,7 +141,7 @@ func validateAndCreateWebhook(c *context.Context, orCtx *orgRepoContext, w *db.W
return
}

field, msg, ok := validateWebhook(c.User, c.Locale, w)
field, msg, ok := validateWebhook(c.Locale, w)
if !ok {
c.FormErr(field)
c.RenderWithErr(msg, orCtx.TmplNew, nil)
Expand Down Expand Up @@ -348,7 +345,7 @@ func validateAndUpdateWebhook(c *context.Context, orCtx *orgRepoContext, w *db.W
return
}

field, msg, ok := validateWebhook(c.User, c.Locale, w)
field, msg, ok := validateWebhook(c.Locale, w)
if !ok {
c.FormErr(field)
c.RenderWithErr(msg, orCtx.TmplNew, nil)
Expand Down
8 changes: 3 additions & 5 deletions internal/route/repo/webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,21 @@ func Test_validateWebhook(t *testing.T) {
}{
{
name: "admin bypass local address check",
actor: &db.User{IsAdmin: true},
webhook: &db.Webhook{URL: "http://localhost:3306"},
webhook: &db.Webhook{URL: "https://www.google.com"},
expOK: true,
},

{
name: "local address not allowed",
actor: &db.User{},
webhook: &db.Webhook{URL: "http://localhost:3306"},
expField: "PayloadURL",
expMsg: "repo.settings.webhook.err_cannot_use_local_addresses",
expMsg: "repo.settings.webhook.url_resolved_to_blocked_local_address",
expOK: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
field, msg, ok := validateWebhook(test.actor, l, test.webhook)
field, msg, ok := validateWebhook(l, test.webhook)
assert.Equal(t, test.expOK, ok)
assert.Equal(t, test.expMsg, msg)
assert.Equal(t, test.expField, field)
Expand Down
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy