diff --git a/modules/hostmatcher/http.go b/modules/hostmatcher/http.go
index 65f5f78b14..c743f6efb3 100644
--- a/modules/hostmatcher/http.go
+++ b/modules/hostmatcher/http.go
@@ -7,12 +7,17 @@ import (
 	"context"
 	"fmt"
 	"net"
+	"net/url"
 	"syscall"
 	"time"
 )
 
 // NewDialContext returns a DialContext for Transport, the DialContext will do allow/block list check
 func NewDialContext(usage string, allowList, blockList *HostMatchList) func(ctx context.Context, network, addr string) (net.Conn, error) {
+	return NewDialContextWithProxy(usage, allowList, blockList, nil)
+}
+
+func NewDialContextWithProxy(usage string, allowList, blockList *HostMatchList, proxy *url.URL) func(ctx context.Context, network, addr string) (net.Conn, error) {
 	// How Go HTTP Client works with redirection:
 	//   transport.RoundTrip URL=http://domain.com, Host=domain.com
 	//   transport.DialContext addrOrHost=domain.com:80
@@ -26,11 +31,18 @@ func NewDialContext(usage string, allowList, blockList *HostMatchList) func(ctx
 			Timeout:   30 * time.Second,
 			KeepAlive: 30 * time.Second,
 
-			Control: func(network, ipAddr string, c syscall.RawConn) (err error) {
-				var host string
-				if host, _, err = net.SplitHostPort(addrOrHost); err != nil {
+			Control: func(network, ipAddr string, c syscall.RawConn) error {
+				host, port, err := net.SplitHostPort(addrOrHost)
+				if err != nil {
 					return err
 				}
+				if proxy != nil {
+					// Always allow the host of the proxy, but only on the specified port.
+					if host == proxy.Hostname() && port == proxy.Port() {
+						return nil
+					}
+				}
+
 				// in Control func, the addr was already resolved to IP:PORT format, there is no cost to do ResolveTCPAddr here
 				tcpAddr, err := net.ResolveTCPAddr(network, ipAddr)
 				if err != nil {
diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go
index 19c34772ca..8f728d3aa6 100644
--- a/services/webhook/deliver.go
+++ b/services/webhook/deliver.go
@@ -239,7 +239,7 @@ var (
 	hostMatchers      []glob.Glob
 )
 
-func webhookProxy() func(req *http.Request) (*url.URL, error) {
+func webhookProxy(allowList *hostmatcher.HostMatchList) func(req *http.Request) (*url.URL, error) {
 	if setting.Webhook.ProxyURL == "" {
 		return proxy.Proxy()
 	}
@@ -257,6 +257,9 @@ func webhookProxy() func(req *http.Request) (*url.URL, error) {
 	return func(req *http.Request) (*url.URL, error) {
 		for _, v := range hostMatchers {
 			if v.Match(req.URL.Host) {
+				if !allowList.MatchHostName(req.URL.Host) {
+					return nil, fmt.Errorf("webhook can only call allowed HTTP servers (check your %s setting), deny '%s'", allowList.SettingKeyHint, req.URL.Host)
+				}
 				return http.ProxyURL(setting.Webhook.ProxyURLFixed)(req)
 			}
 		}
@@ -278,8 +281,8 @@ func Init() error {
 		Timeout: timeout,
 		Transport: &http.Transport{
 			TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify},
-			Proxy:           webhookProxy(),
-			DialContext:     hostmatcher.NewDialContext("webhook", allowedHostMatcher, nil),
+			Proxy:           webhookProxy(allowedHostMatcher),
+			DialContext:     hostmatcher.NewDialContextWithProxy("webhook", allowedHostMatcher, nil, setting.Webhook.ProxyURLFixed),
 		},
 	}
 
diff --git a/services/webhook/deliver_test.go b/services/webhook/deliver_test.go
index ee63975ad3..72aa00478a 100644
--- a/services/webhook/deliver_test.go
+++ b/services/webhook/deliver_test.go
@@ -14,35 +14,72 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/unittest"
 	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/hostmatcher"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestWebhookProxy(t *testing.T) {
+	oldWebhook := setting.Webhook
+	t.Cleanup(func() {
+		setting.Webhook = oldWebhook
+	})
+
 	setting.Webhook.ProxyURL = "http://localhost:8080"
 	setting.Webhook.ProxyURLFixed, _ = url.Parse(setting.Webhook.ProxyURL)
 	setting.Webhook.ProxyHosts = []string{"*.discordapp.com", "discordapp.com"}
 
-	kases := map[string]string{
-		"https://discordapp.com/api/webhooks/xxxxxxxxx/xxxxxxxxxxxxxxxxxxx": "http://localhost:8080",
-		"http://s.discordapp.com/assets/xxxxxx":                             "http://localhost:8080",
-		"http://github.com/a/b":                                             "",
+	allowedHostMatcher := hostmatcher.ParseHostMatchList("webhook.ALLOWED_HOST_LIST", "discordapp.com,s.discordapp.com")
+
+	tests := []struct {
+		req     string
+		want    string
+		wantErr bool
+	}{
+		{
+			req:     "https://discordapp.com/api/webhooks/xxxxxxxxx/xxxxxxxxxxxxxxxxxxx",
+			want:    "http://localhost:8080",
+			wantErr: false,
+		},
+		{
+			req:     "http://s.discordapp.com/assets/xxxxxx",
+			want:    "http://localhost:8080",
+			wantErr: false,
+		},
+		{
+			req:     "http://github.com/a/b",
+			want:    "",
+			wantErr: false,
+		},
+		{
+			req:     "http://www.discordapp.com/assets/xxxxxx",
+			want:    "",
+			wantErr: true,
+		},
 	}
+	for _, tt := range tests {
+		t.Run(tt.req, func(t *testing.T) {
+			req, err := http.NewRequest("POST", tt.req, nil)
+			require.NoError(t, err)
 
-	for reqURL, proxyURL := range kases {
-		req, err := http.NewRequest("POST", reqURL, nil)
-		assert.NoError(t, err)
+			u, err := webhookProxy(allowedHostMatcher)(req)
+			if tt.wantErr {
+				assert.Error(t, err)
+				return
+			}
 
-		u, err := webhookProxy()(req)
-		assert.NoError(t, err)
-		if proxyURL == "" {
-			assert.Nil(t, u)
-		} else {
-			assert.EqualValues(t, proxyURL, u.String())
-		}
+			assert.NoError(t, err)
+
+			got := ""
+			if u != nil {
+				got = u.String()
+			}
+			assert.Equal(t, tt.want, got)
+		})
 	}
 }