diff --git a/internal/handlers/export_test.go b/internal/handlers/export_test.go index eb5d48d..831ed96 100644 --- a/internal/handlers/export_test.go +++ b/internal/handlers/export_test.go @@ -12,3 +12,13 @@ func (s *Handlers) RenderTemplateForTest( ) { s.renderTemplate(w, r, pageTemplate, data) } + +// BuildSlackTargetConfigForTest exposes buildSlackTargetConfig +// for use in the handlers_test package. +func (s *Handlers) BuildSlackTargetConfigForTest( + w http.ResponseWriter, + r *http.Request, + targetURL string, +) (string, error) { + return s.buildSlackTargetConfig(w, r, targetURL) +} diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go index 679d2e2..ec2791b 100644 --- a/internal/handlers/handlers_test.go +++ b/internal/handlers/handlers_test.go @@ -116,6 +116,52 @@ func TestHandleIndex_Authenticated(t *testing.T) { ) } +func TestBuildSlackTargetConfig_AcceptsPublicURL(t *testing.T) { + t.Parallel() + + var h *handlers.Handlers + + app := newTestApp(t, &h) + app.RequireStart() + + t.Cleanup(app.RequireStop) + + req := httptest.NewRequestWithContext( + context.Background(), http.MethodPost, "/", nil) + w := httptest.NewRecorder() + + cfg, err := h.BuildSlackTargetConfigForTest( + w, req, "http://93.184.216.34/services/T00/B00/xxx", + ) + + require.NoError(t, err) + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, cfg, "webhookUrl") +} + +func TestBuildSlackTargetConfig_RejectsReservedURL(t *testing.T) { + t.Parallel() + + var h *handlers.Handlers + + app := newTestApp(t, &h) + app.RequireStart() + + t.Cleanup(app.RequireStop) + + req := httptest.NewRequestWithContext( + context.Background(), http.MethodPost, "/", nil) + w := httptest.NewRecorder() + + cfg, err := h.BuildSlackTargetConfigForTest( + w, req, "http://169.254.169.254/latest/meta-data/", + ) + + require.Error(t, err) + assert.Empty(t, cfg) + assert.Equal(t, http.StatusBadRequest, w.Code) +} + func TestRenderTemplate(t *testing.T) { t.Parallel() diff --git a/internal/handlers/source_management.go b/internal/handlers/source_management.go index 2622c84..917307a 100644 --- a/internal/handlers/source_management.go +++ b/internal/handlers/source_management.go @@ -902,7 +902,7 @@ func (h *Handlers) buildTargetConfig( case database.TargetTypeHTTP: return h.buildHTTPTargetConfig(w, r, targetURL) case database.TargetTypeSlack: - return h.buildSlackTargetConfig(w, targetURL) + return h.buildSlackTargetConfig(w, r, targetURL) case database.TargetTypeDatabase, database.TargetTypeLog: return "", nil default: @@ -967,6 +967,7 @@ func (h *Handlers) buildHTTPTargetConfig( // buildSlackTargetConfig builds config JSON for a Slack target. func (h *Handlers) buildSlackTargetConfig( w http.ResponseWriter, + r *http.Request, targetURL string, ) (string, error) { if targetURL == "" { @@ -979,6 +980,24 @@ func (h *Handlers) buildSlackTargetConfig( return "", errMissingURL } + err := delivery.ValidateTargetURL( + r.Context(), targetURL, + ) + if err != nil { + h.log.Warn( + "target URL blocked by SSRF protection", + "url", targetURL, + "error", err, + ) + http.Error( + w, + "Invalid target URL: "+err.Error(), + http.StatusBadRequest, + ) + + return "", err + } + cfg := map[string]any{"webhookUrl": targetURL} configBytes, err := json.Marshal(cfg)