CVE-2022-1285 (published 2022-06-01T06:15Z)

Server-Side Request Forgery (SSRF) in GitHub repository gogs/gogs prior to 0.12.8.

Severity: MEDIUM
CVSS v2 base score: 4.3 (AV:N/AC:M/Au:N/C:P/I:N/A:N)
CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-918
cwe_name: Server-Side Request Forgery (SSRF)
description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
extended_description: By providing URLs to unexpected hosts or ports, attackers can make it appear that the server is sending the request, possibly bypassing access controls such as firewalls that prevent the attackers from accessing the URLs directly. The server can be used as a proxy to conduct port scanning of hosts in internal networks, use other URLs such as that can access documents on the system (using file://), or use other protocols such as gopher:// or tftp://, which may provide greater control over the contents of requests.
         

Fix information:
repo_url: https://github.com/gogs/gogs
commit_details: repo_url: https://github.com/gogs/gogs
author: Joe Chen
author_date: 2022-05-31 15:17:17+08:00
msg: webhook: revalidate local hostname before each delivery (#6988)
repository: repo_url: https://github.com/gogs/gogs
repo_name: gogs/gogs
description: Gogs is a painless self-hosted Git service
repo_language: Go
owner: gogs
file_changes: filename: CHANGELOG.md
change_type: ModificationType.MODIFY
diff: @@ -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)
 - _Security:_ XSS in cookies. [#6953](https://github.com/gogs/gogs/issues/6953)
 - _Security:_ OS Command Injection in file uploading. [#6968](https://github.com/gogs/gogs/issues/6968)
 - _Security:_ Remote Command Execution in file editing. [#6555](https://github.com/gogs/gogs/issues/6555)

programming_language: Markdown
filename: locale_en-US.ini
change_type: ModificationType.MODIFY
diff: @@ -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
@@ -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

programming_language: Ruby
filename: error.go
change_type: ModificationType.MODIFY
diff: @@ -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 {
@@ -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 {

programming_language: Go
filename: webhook.go
change_type: ModificationType.MODIFY
diff: @@ -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"
 )
 
@@ -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

programming_language: Go
filename: repo.go
change_type: ModificationType.MODIFY
diff: @@ -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 {

programming_language: Go
filename: netutil.go
change_type: ModificationType.MODIFY
diff: @@ -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

programming_language: Go
filename: netutil_test.go
change_type: ModificationType.MODIFY
diff: @@ -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))
 		})
 	}
 }

programming_language: Go
filename: repo.go
change_type: ModificationType.MODIFY
diff: @@ -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")
 			}

programming_language: Go
filename: repo.go
change_type: ModificationType.MODIFY
diff: @@ -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")
 			}

programming_language: Go
filename: webhook.go
change_type: ModificationType.MODIFY
diff: @@ -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
 }
 
@@ -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)
@@ -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)

programming_language: Go
filename: webhook_test.go
change_type: ModificationType.MODIFY
diff: @@ -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)

programming_language: Go