package handlers_test import ( "context" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm/clause" "sneak.berlin/go/webhooker/internal/database" "sneak.berlin/go/webhooker/internal/handlers" "sneak.berlin/go/webhooker/internal/session" ) // The secret path segments of a Slack incoming webhook URL. // Holding them is enough to post to the channel forever, so // they must never reach the rendered page. const ( slackSecretPath = "/services/T00000000/B00000000/" + "XXXXXXXXXXXXXXXXXXXXXXXX" slackWebhookURL = "https://hooks.slack.com" + slackSecretPath ) // seedConfiguredTarget inserts a target with a stored config // blob and returns it. func seedConfiguredTarget( t *testing.T, db *database.Database, webhookID string, targetType database.TargetType, config string, ) *database.Target { t.Helper() tgt := &database.Target{ WebhookID: webhookID, Name: "t-" + string(targetType), Type: targetType, Active: true, Config: config, } require.NoError( t, db.DB().Omit(clause.Associations).Create(tgt).Error, ) return tgt } // renderSourceDetailPage runs the real source detail handler // for a webhook and returns the rendered HTML. func renderSourceDetailPage( t *testing.T, h *handlers.Handlers, sess *session.Session, webhookID string, ) string { t.Helper() req := httptest.NewRequestWithContext( context.Background(), http.MethodGet, "/source/"+webhookID, nil, ) for _, c := range authenticatedCookies( t, sess, deleteTestUserID, deleteTestUsername, ) { req.AddCookie(c) } rctx := chi.NewRouteContext() rctx.URLParams.Add(paramSourceID, webhookID) req = req.WithContext( context.WithValue( req.Context(), chi.RouteCtxKey, rctx, ), ) w := httptest.NewRecorder() h.HandleSourceDetail().ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) return w.Body.String() } // TestHandleSourceDetail_MasksSlackWebhookURL is the // load-bearing regression test for the credential leak: the // rendered page must show the Slack target without any of the // secret path segments of its webhook URL. func TestHandleSourceDetail_MasksSlackWebhookURL(t *testing.T) { t.Parallel() var ( h *handlers.Handlers sess *session.Session db *database.Database ) app := newTestApp(t, &h, &sess, &db) app.RequireStart() t.Cleanup(app.RequireStop) wh := seedWebhook(t, db) seedConfiguredTarget( t, db, wh.ID, database.TargetTypeSlack, `{"webhookUrl":"`+slackWebhookURL+`"}`, ) body := renderSourceDetailPage(t, h, sess, wh.ID) assert.NotContains(t, body, slackSecretPath) assert.NotContains(t, body, "T00000000") assert.NotContains(t, body, "B00000000") assert.NotContains( t, body, "XXXXXXXXXXXXXXXXXXXXXXXX", ) assert.NotContains(t, body, "webhookUrl") assert.Contains(t, body, "Webhook URL") assert.Contains(t, body, "https://hooks.slack.com/...") } // TestHandleSourceDetail_MasksHTTPDestinationURL is the // regression test for the same leak reached through the http // target: its destination is routinely an incoming-webhook // endpoint whose path segments are the credential, so the // rendered page must not contain them. func TestHandleSourceDetail_MasksHTTPDestinationURL( t *testing.T, ) { t.Parallel() var ( h *handlers.Handlers sess *session.Session db *database.Database ) app := newTestApp(t, &h, &sess, &db) app.RequireStart() t.Cleanup(app.RequireStop) wh := seedWebhook(t, db) seedConfiguredTarget( t, db, wh.ID, database.TargetTypeHTTP, `{"url":"`+slackWebhookURL+`"}`, ) body := renderSourceDetailPage(t, h, sess, wh.ID) assert.NotContains(t, body, slackSecretPath) assert.NotContains(t, body, "T00000000") assert.NotContains(t, body, "B00000000") assert.NotContains( t, body, "XXXXXXXXXXXXXXXXXXXXXXXX", ) assert.Contains(t, body, "Destination URL") assert.Contains(t, body, "https://hooks.slack.com/...") } // TestHandleSourceDetail_RendersNamedTargetFields proves the // other target types render labelled fields rather than the // stored blob. func TestHandleSourceDetail_RendersNamedTargetFields( t *testing.T, ) { t.Parallel() var ( h *handlers.Handlers sess *session.Session db *database.Database ) app := newTestApp(t, &h, &sess, &db) app.RequireStart() t.Cleanup(app.RequireStop) wh := seedWebhook(t, db) seedConfiguredTarget( t, db, wh.ID, database.TargetTypeHTTP, `{"url":"https://example.com/hook","timeout":30,`+ `"headers":{"Authorization":"Bearer sekrit"}}`, ) seedConfiguredTarget( t, db, wh.ID, database.TargetTypeDatabase, `{"expiry":"720h"}`, ) seedConfiguredTarget( t, db, wh.ID, database.TargetType("carrier-pigeon"), `{"beak":"sharp"}`, ) body := renderSourceDetailPage(t, h, sess, wh.ID) assert.Contains(t, body, "Destination URL") assert.Contains(t, body, "https://example.com/...") assert.Contains(t, body, "Timeout") assert.Contains(t, body, "1 configured") assert.NotContains(t, body, "sekrit") assert.Contains(t, body, "Archive Expiry") assert.Contains(t, body, "720h") // An unknown type gets the neutral placeholder, never the // stored blob. assert.Contains(t, body, "(unavailable)") assert.NotContains(t, body, "beak") }