diff options
author | Giteabot <teabot@gitea.io> | 2023-10-18 21:07:20 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-10-18 15:07:20 +0200 |
commit | ca4418eff12d92a4da29bba4331451bf6cd0b620 (patch) | |
tree | ca00e6c5ce55ba1c0eb3c78f0680cae27ca537f1 /services | |
parent | 80c0c8815203128703eae741e712289393458687 (diff) | |
download | gitea-ca4418eff12d92a4da29bba4331451bf6cd0b620.tar.gz gitea-ca4418eff12d92a4da29bba4331451bf6cd0b620.zip |
Support allowed hosts for webhook to work with proxy (#27655) (#27674)
Backport #27655 by @wolfogre
When `webhook.PROXY_URL` has been set, the old code will check if the
proxy host is in `ALLOWED_HOST_LIST` or reject requests through the
proxy. It requires users to add the proxy host to `ALLOWED_HOST_LIST`.
However, it actually allows all requests to any port on the host, when
the proxy host is probably an internal address.
But things may be even worse. `ALLOWED_HOST_LIST` doesn't really work
when requests are sent to the allowed proxy, and the proxy could forward
them to any hosts.
This PR fixes it by:
- If the proxy has been set, always allow connectioins to the host and
port.
- Check `ALLOWED_HOST_LIST` before forwarding.
Co-authored-by: Jason Song <i@wolfogre.com>
Diffstat (limited to 'services')
-rw-r--r-- | services/webhook/deliver.go | 9 | ||||
-rw-r--r-- | services/webhook/deliver_test.go | 67 |
2 files changed, 58 insertions, 18 deletions
diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go index fd7a3d7fba..176ba83e67 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) + + u, err := webhookProxy(allowedHostMatcher)(req) + if tt.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) - for reqURL, proxyURL := range kases { - req, err := http.NewRequest("POST", reqURL, nil) - assert.NoError(t, err) - - u, err := webhookProxy()(req) - assert.NoError(t, err) - if proxyURL == "" { - assert.Nil(t, u) - } else { - assert.EqualValues(t, proxyURL, u.String()) - } + got := "" + if u != nil { + got = u.String() + } + assert.Equal(t, tt.want, got) + }) } } |