diff --git a/notification-dingtalk/dingtalk_notification.go b/notification-dingtalk/dingtalk_notification.go index f295a20a..7e313516 100644 --- a/notification-dingtalk/dingtalk_notification.go +++ b/notification-dingtalk/dingtalk_notification.go @@ -110,6 +110,10 @@ func (n *Notification) Notify(msg plugin.NotificationMessage) { log.Errorf("user %s has no webhook url", msg.ReceiverUserID) return } + if err := validateWebhookURL(userConfig.WebhookURL); err != nil { + log.Errorf("user %s has an invalid webhook url", msg.ReceiverUserID) + return + } notificationMsg, notificationTitle := renderNotification(msg) // no need to send empty message @@ -119,7 +123,7 @@ func (n *Notification) Notify(msg plugin.NotificationMessage) { } // Create a Resty Client - client := resty.New() + client := resty.New().SetRedirectPolicy(resty.NoRedirectPolicy()) resp, err := client.R(). SetHeader("Content-Type", "application/json"). SetBody(NewWebhookReq(notificationMsg, notificationTitle)). diff --git a/notification-dingtalk/user_config.go b/notification-dingtalk/user_config.go index 8ec58f81..91b3e6df 100644 --- a/notification-dingtalk/user_config.go +++ b/notification-dingtalk/user_config.go @@ -22,6 +22,8 @@ package dingtalk import ( "encoding/json" "fmt" + "net/url" + "strings" "sync" "github.com/apache/answer-plugins/notification-dingtalk/i18n" @@ -68,6 +70,15 @@ func (ucc *UserConfigCache) NewQuestionSubscriberIDs() []string { return userIDs } +func validateWebhookURL(rawURL string) error { + u, err := url.Parse(rawURL) + if err != nil || !strings.EqualFold(u.Scheme, "https") || !strings.EqualFold(u.Hostname(), "oapi.dingtalk.com") || + (u.Port() != "" && u.Port() != "443") || u.User != nil || u.Fragment != "" { + return fmt.Errorf("invalid webhook url") + } + return nil +} + func (n *Notification) UserConfigFields() []plugin.ConfigField { fields := make([]plugin.ConfigField, 0) // Show tip for user, if the notification service is disabled @@ -132,6 +143,11 @@ func (n *Notification) UserConfigReceiver(userID string, config []byte) error { if err != nil { return fmt.Errorf("unmarshal user config failed: %w", err) } + if len(userConfig.WebhookURL) > 0 { + if err := validateWebhookURL(userConfig.WebhookURL); err != nil { + return err + } + } n.UserConfigCache.SetUserConfig(userID, &userConfig) return nil } diff --git a/notification-dingtalk/webhook_url_test.go b/notification-dingtalk/webhook_url_test.go new file mode 100644 index 00000000..d581c8ae --- /dev/null +++ b/notification-dingtalk/webhook_url_test.go @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0. + */ + +package dingtalk + +import "testing" + +func TestValidateWebhookURL(t *testing.T) { + for _, test := range []struct { + url string + want bool + }{ + {"https://oapi.dingtalk.com/robot/send?access_token=test", true}, + {"http://oapi.dingtalk.com/robot/send", false}, + {"https://127.0.0.1/robot/send", false}, + {"https://oapi.dingtalk.com.evil.test/robot/send", false}, + {"https://oapi.dingtalk.com:8443/robot/send", false}, + {"https://user@oapi.dingtalk.com/robot/send", false}, + } { + err := validateWebhookURL(test.url) + if (err == nil) != test.want { + t.Errorf("validateWebhookURL(%q) error = %v, want success = %t", test.url, err, test.want) + } + } +} diff --git a/notification-slack/slack_notification.go b/notification-slack/slack_notification.go index dfb3ea39..29633a1b 100644 --- a/notification-slack/slack_notification.go +++ b/notification-slack/slack_notification.go @@ -129,6 +129,10 @@ func (n *Notification) Notify(msg plugin.NotificationMessage) { log.Errorf("user %s has no webhook url", msg.ReceiverUserID) return } + if err := validateWebhookURL(userConfig.WebhookURL); err != nil { + log.Errorf("user %s has an invalid webhook url", msg.ReceiverUserID) + return + } notificationMsg := renderNotification(msg) // no need to send empty message @@ -138,7 +142,7 @@ func (n *Notification) Notify(msg plugin.NotificationMessage) { } // Create a Resty Client - client := resty.New() + client := resty.New().SetRedirectPolicy(resty.NoRedirectPolicy()) resp, err := client.R(). SetHeader("Content-Type", "application/json"). SetBody(NewWebhookReq(notificationMsg)). diff --git a/notification-slack/user_config.go b/notification-slack/user_config.go index d37c02e0..2b6695e0 100644 --- a/notification-slack/user_config.go +++ b/notification-slack/user_config.go @@ -22,6 +22,8 @@ package slack_notification import ( "encoding/json" "fmt" + "net/url" + "strings" "sync" "github.com/apache/answer-plugins/notification-slack/i18n" @@ -72,6 +74,15 @@ func (ucc *UserConfigCache) NewQuestionSubscriberIDs() []string { return userIDs } +func validateWebhookURL(rawURL string) error { + u, err := url.Parse(rawURL) + if err != nil || !strings.EqualFold(u.Scheme, "https") || !strings.EqualFold(u.Hostname(), "hooks.slack.com") || + (u.Port() != "" && u.Port() != "443") || u.User != nil || u.Fragment != "" { + return fmt.Errorf("invalid webhook url") + } + return nil +} + func (n *Notification) UserConfigFields() []plugin.ConfigField { fields := make([]plugin.ConfigField, 0) // Show tip for user, if the notification service is disabled @@ -160,6 +171,11 @@ func (n *Notification) UserConfigReceiver(userID string, config []byte) error { if err != nil { return fmt.Errorf("unmarshal user config failed: %w", err) } + if len(userConfig.WebhookURL) > 0 { + if err := validateWebhookURL(userConfig.WebhookURL); err != nil { + return err + } + } n.UserConfigCache.SetUserConfig(userID, &userConfig) return nil } diff --git a/notification-slack/webhook_url_test.go b/notification-slack/webhook_url_test.go new file mode 100644 index 00000000..d1423e00 --- /dev/null +++ b/notification-slack/webhook_url_test.go @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0. + */ + +package slack_notification + +import "testing" + +func TestValidateWebhookURL(t *testing.T) { + for _, test := range []struct { + url string + want bool + }{ + {"https://hooks.slack.com/services/test", true}, + {"http://hooks.slack.com/services/test", false}, + {"https://127.0.0.1/services/test", false}, + {"https://hooks.slack.com.evil.test/services/test", false}, + {"https://hooks.slack.com:8443/services/test", false}, + {"https://user@hooks.slack.com/services/test", false}, + } { + err := validateWebhookURL(test.url) + if (err == nil) != test.want { + t.Errorf("validateWebhookURL(%q) error = %v, want success = %t", test.url, err, test.want) + } + } +} diff --git a/notification-wecom/notification_wecom.go b/notification-wecom/notification_wecom.go index afdc0fde..941e6080 100644 --- a/notification-wecom/notification_wecom.go +++ b/notification-wecom/notification_wecom.go @@ -109,6 +109,10 @@ func (n *Notification) Notify(msg plugin.NotificationMessage) { log.Errorf("user %s has no webhook url", msg.ReceiverUserID) return } + if err := validateWebhookURL(userConfig.WebhookURL); err != nil { + log.Errorf("user %s has an invalid webhook url", msg.ReceiverUserID) + return + } notificationMsg := renderNotification(msg) // no need to send empty message @@ -118,7 +122,7 @@ func (n *Notification) Notify(msg plugin.NotificationMessage) { } // Create a Resty Client - client := resty.New() + client := resty.New().SetRedirectPolicy(resty.NoRedirectPolicy()) resp, err := client.R(). SetHeader("Content-Type", "application/json"). SetBody(NewWebhookReq(notificationMsg)). diff --git a/notification-wecom/user_config.go b/notification-wecom/user_config.go index 17ff018c..7acb742b 100644 --- a/notification-wecom/user_config.go +++ b/notification-wecom/user_config.go @@ -22,6 +22,8 @@ package wecom import ( "encoding/json" "fmt" + "net/url" + "strings" "sync" "github.com/apache/answer-plugins/notification-wecom/i18n" @@ -68,6 +70,15 @@ func (ucc *UserConfigCache) NewQuestionSubscriberIDs() []string { return userIDs } +func validateWebhookURL(rawURL string) error { + u, err := url.Parse(rawURL) + if err != nil || !strings.EqualFold(u.Scheme, "https") || !strings.EqualFold(u.Hostname(), "qyapi.weixin.qq.com") || + (u.Port() != "" && u.Port() != "443") || u.User != nil || u.Fragment != "" { + return fmt.Errorf("invalid webhook url") + } + return nil +} + func (n *Notification) UserConfigFields() []plugin.ConfigField { fields := make([]plugin.ConfigField, 0) // Show tip for user, if the notification service is disabled @@ -132,6 +143,11 @@ func (n *Notification) UserConfigReceiver(userID string, config []byte) error { if err != nil { return fmt.Errorf("unmarshal user config failed: %w", err) } + if len(userConfig.WebhookURL) > 0 { + if err := validateWebhookURL(userConfig.WebhookURL); err != nil { + return err + } + } n.UserConfigCache.SetUserConfig(userID, &userConfig) return nil } diff --git a/notification-wecom/webhook_url_test.go b/notification-wecom/webhook_url_test.go new file mode 100644 index 00000000..23fed76c --- /dev/null +++ b/notification-wecom/webhook_url_test.go @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0. + */ + +package wecom + +import "testing" + +func TestValidateWebhookURL(t *testing.T) { + for _, test := range []struct { + url string + want bool + }{ + {"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=test", true}, + {"http://qyapi.weixin.qq.com/cgi-bin/webhook/send", false}, + {"https://127.0.0.1/cgi-bin/webhook/send", false}, + {"https://qyapi.weixin.qq.com.evil.test/cgi-bin/webhook/send", false}, + {"https://qyapi.weixin.qq.com:8443/cgi-bin/webhook/send", false}, + {"https://user@qyapi.weixin.qq.com/cgi-bin/webhook/send", false}, + } { + err := validateWebhookURL(test.url) + if (err == nil) != test.want { + t.Errorf("validateWebhookURL(%q) error = %v, want success = %t", test.url, err, test.want) + } + } +} diff --git a/user-center-slack/notification.go b/user-center-slack/notification.go index a75a5029..6c627971 100644 --- a/user-center-slack/notification.go +++ b/user-center-slack/notification.go @@ -97,6 +97,10 @@ func (uc *UserCenter) Notify(msg plugin.NotificationMessage) { log.Errorf("user %s has no webhook url", msg.ReceiverUserID) return } + if err := validateWebhookURL(userConfig.WebhookURL); err != nil { + log.Errorf("user %s has an invalid webhook url", msg.ReceiverUserID) + return + } notificationMsg := renderNotification(msg) // no need to send empty message @@ -106,7 +110,7 @@ func (uc *UserCenter) Notify(msg plugin.NotificationMessage) { } // Create a Resty Client - client := resty.New() + client := resty.New().SetRedirectPolicy(resty.NoRedirectPolicy()) resp, err := client.R(). SetHeader("Content-Type", "application/json"). SetBody(NewWebhookReq(notificationMsg)). diff --git a/user-center-slack/user_config.go b/user-center-slack/user_config.go index 92d1f48f..67abe35e 100644 --- a/user-center-slack/user_config.go +++ b/user-center-slack/user_config.go @@ -22,6 +22,8 @@ package slack_user_center import ( "encoding/json" "fmt" + "net/url" + "strings" "sync" "github.com/apache/answer-plugins/user-center-slack/i18n" @@ -72,6 +74,15 @@ func (ucc *UserConfigCache) NewQuestionSubscriberIDs() []string { return userIDs } +func validateWebhookURL(rawURL string) error { + u, err := url.Parse(rawURL) + if err != nil || !strings.EqualFold(u.Scheme, "https") || !strings.EqualFold(u.Hostname(), "hooks.slack.com") || + (u.Port() != "" && u.Port() != "443") || u.User != nil || u.Fragment != "" { + return fmt.Errorf("invalid webhook url") + } + return nil +} + func (uc *UserCenter) UserConfigFields() []plugin.ConfigField { fields := make([]plugin.ConfigField, 0) // Show tip for user, if the notification service is disabled @@ -160,6 +171,11 @@ func (uc *UserCenter) UserConfigReceiver(userID string, config []byte) error { if err != nil { return fmt.Errorf("unmarshal user config failed: %w", err) } + if len(userConfig.WebhookURL) > 0 { + if err := validateWebhookURL(userConfig.WebhookURL); err != nil { + return err + } + } uc.UserConfigCache.SetUserConfig(userID, &userConfig) return nil } diff --git a/user-center-slack/webhook_url_test.go b/user-center-slack/webhook_url_test.go new file mode 100644 index 00000000..f754aa0b --- /dev/null +++ b/user-center-slack/webhook_url_test.go @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0. + */ + +package slack_user_center + +import "testing" + +func TestValidateWebhookURL(t *testing.T) { + for _, test := range []struct { + url string + want bool + }{ + {"https://hooks.slack.com/services/test", true}, + {"http://hooks.slack.com/services/test", false}, + {"https://127.0.0.1/services/test", false}, + {"https://hooks.slack.com.evil.test/services/test", false}, + {"https://hooks.slack.com:8443/services/test", false}, + {"https://user@hooks.slack.com/services/test", false}, + } { + err := validateWebhookURL(test.url) + if (err == nil) != test.want { + t.Errorf("validateWebhookURL(%q) error = %v, want success = %t", test.url, err, test.want) + } + } +}