Source detail page renders raw target config, exposing the Slack webhook URL credential #113
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
templates/source_detail.html:148-149renders the stored target config verbatim:For a
slacktarget that config is{"webhookUrl": "https://hooks.slack.com/services/..."}(internal/delivery/target_slack.go:26-28). A Slack incoming webhook URL is a bearer credential — anyone holding it can post to the channel indefinitely, and it cannot be scoped or attributed. It is currently rendered in full to every user who can load the source detail page, and so also reaches browser history, screenshots, and any support screen-share.Milestoned 1.0.0 because that milestone is internet-facing readiness and this ships a credential into the UI.
Definition of done
slack— the webhook URL is masked: show scheme plus host plus an elided path (e.g.https://hooks.slack.com/services/...), never the full path segments, which are the secret.http— show the destination URL and retry settings as labelled fields.database— show the configured archive expiry as a labelled field.slacktarget does not contain the secret path segments of the configured webhook URL. This is the load-bearing test; without it the regression returns silently.Implementation requirements
next, PR based onnext, single commit, title ending(closes #N).make check, and run the Docker lint path viascript/cibuildwith the cache defeated.Plan:
internal/delivery/target_config_view.go:ConfigField{Label,Value}plusTargetView, a display-safe projection ofdatabase.Targetthat carries named config fields and no rawConfigstring — so a template has nothing to leak.(*SlackTargetConfig).MaskedWebhookURL()reduces the URL to scheme + host with the whole path, query and userinfo elided (https://hooks.slack.com/...). Eliding every path segment rather than keeping the first is deliberate: the field accepts any URL, so no segment can be assumed non-secret.slack→ masked webhook URL;http→ destination URL, timeout, max retries, max queue size, and a header count (header values can carry bearer tokens, so they are not rendered);database→ archive expiry (empty =never);log→ none.Configuration: (unavailable)field. No raw-blob fallback on any path.renderSourceDetailpassesdelivery.NewTargetViews(targets);templates/source_detail.htmlranges the named fields using existing Tailwind classes only.slacktarget and asserts the webhook URL's secret path segments are absent from the HTML.clawbot referenced this issue2026-08-11 14:31:20 +02:00