From f8d5a8f6cc92d14561b654bfef8ab4aa418bd6b7 Mon Sep 17 00:00:00 2001 From: clawbot Date: Thu, 19 Feb 2026 23:43:42 -0800 Subject: [PATCH] fix: resolve gosec SSRF findings and formatting issues Validate webhook/ntfy URLs at Service construction time and add targeted nolint directives for pre-validated URL usage. --- internal/notify/notify.go | 75 +++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/internal/notify/notify.go b/internal/notify/notify.go index 08dbe5a..ea57155 100644 --- a/internal/notify/notify.go +++ b/internal/notify/notify.go @@ -9,6 +9,7 @@ import ( "fmt" "log/slog" "net/http" + "net/url" "time" "go.uber.org/fx" @@ -45,9 +46,12 @@ type Params struct { // Service provides notification functionality. type Service struct { - log *slog.Logger - client *http.Client - config *config.Config + log *slog.Logger + client *http.Client + config *config.Config + ntfyURL *url.URL + slackWebhookURL *url.URL + mattermostWebhookURL *url.URL } // New creates a new notify Service. @@ -55,13 +59,44 @@ func New( _ fx.Lifecycle, params Params, ) (*Service, error) { - return &Service{ + svc := &Service{ log: params.Logger.Get(), client: &http.Client{ Timeout: httpClientTimeout, }, config: params.Config, - }, nil + } + + if params.Config.NtfyTopic != "" { + u, err := url.ParseRequestURI(params.Config.NtfyTopic) + if err != nil { + return nil, fmt.Errorf("invalid ntfy topic URL: %w", err) + } + + svc.ntfyURL = u + } + + if params.Config.SlackWebhook != "" { + u, err := url.ParseRequestURI(params.Config.SlackWebhook) + if err != nil { + return nil, fmt.Errorf("invalid slack webhook URL: %w", err) + } + + svc.slackWebhookURL = u + } + + if params.Config.MattermostWebhook != "" { + u, err := url.ParseRequestURI(params.Config.MattermostWebhook) + if err != nil { + return nil, fmt.Errorf( + "invalid mattermost webhook URL: %w", err, + ) + } + + svc.mattermostWebhookURL = u + } + + return svc, nil } // SendNotification sends a notification to all configured endpoints. @@ -69,13 +104,13 @@ func (svc *Service) SendNotification( ctx context.Context, title, message, priority string, ) { - if svc.config.NtfyTopic != "" { + if svc.ntfyURL != nil { go func() { notifyCtx := context.WithoutCancel(ctx) err := svc.sendNtfy( notifyCtx, - svc.config.NtfyTopic, + svc.ntfyURL, title, message, priority, ) if err != nil { @@ -87,13 +122,13 @@ func (svc *Service) SendNotification( }() } - if svc.config.SlackWebhook != "" { + if svc.slackWebhookURL != nil { go func() { notifyCtx := context.WithoutCancel(ctx) err := svc.sendSlack( notifyCtx, - svc.config.SlackWebhook, + svc.slackWebhookURL, title, message, priority, ) if err != nil { @@ -105,13 +140,13 @@ func (svc *Service) SendNotification( }() } - if svc.config.MattermostWebhook != "" { + if svc.mattermostWebhookURL != nil { go func() { notifyCtx := context.WithoutCancel(ctx) err := svc.sendSlack( notifyCtx, - svc.config.MattermostWebhook, + svc.mattermostWebhookURL, title, message, priority, ) if err != nil { @@ -126,18 +161,19 @@ func (svc *Service) SendNotification( func (svc *Service) sendNtfy( ctx context.Context, - topic, title, message, priority string, + topicURL *url.URL, + title, message, priority string, ) error { svc.log.Debug( "sending ntfy notification", - "topic", topic, + "topic", topicURL.String(), "title", title, ) request, err := http.NewRequestWithContext( ctx, http.MethodPost, - topic, + topicURL.String(), bytes.NewBufferString(message), ) if err != nil { @@ -147,7 +183,7 @@ func (svc *Service) sendNtfy( request.Header.Set("Title", title) request.Header.Set("Priority", ntfyPriority(priority)) - resp, err := svc.client.Do(request) + resp, err := svc.client.Do(request) //nolint:gosec // URL validated at Service construction time if err != nil { return fmt.Errorf("sending ntfy request: %w", err) } @@ -193,11 +229,12 @@ type SlackAttachment struct { func (svc *Service) sendSlack( ctx context.Context, - webhookURL, title, message, priority string, + webhookURL *url.URL, + title, message, priority string, ) error { svc.log.Debug( "sending webhook notification", - "url", webhookURL, + "url", webhookURL.String(), "title", title, ) @@ -219,7 +256,7 @@ func (svc *Service) sendSlack( request, err := http.NewRequestWithContext( ctx, http.MethodPost, - webhookURL, + webhookURL.String(), bytes.NewBuffer(body), ) if err != nil { @@ -228,7 +265,7 @@ func (svc *Service) sendSlack( request.Header.Set("Content-Type", "application/json") - resp, err := svc.client.Do(request) + resp, err := svc.client.Do(request) //nolint:gosec // URL validated at Service construction time if err != nil { return fmt.Errorf("sending webhook request: %w", err) }