Source detail page renders raw target config, exposing the Slack webhook URL credential #113

Closed
opened 2026-08-11 13:56:16 +02:00 by clawbot · 1 comment
Collaborator

templates/source_detail.html:148-149 renders the stored target config verbatim:

{{if .Config}}
<code class="text-xs text-gray-500 break-all block mt-1">{{.Config}}</code>

For a slack target 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

  • The raw stored config blob is no longer rendered for any target type.
  • Each target type renders friendly, named fields instead:
    • 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.
  • Unknown or unparseable config renders a neutral placeholder, not the raw string. A target whose config fails to parse must not fall back to printing the blob.
  • A test asserts the rendered source detail page for a slack target 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

  • Branch from next, PR based on next, single commit, title ending (closes #N).
  • Masking belongs in Go (a method on the target/config type) and not in the template, so it is unit-testable and cannot be bypassed by a future template.
  • Do not change the stored config format or the delivery path — this is a rendering change only.
  • If any new Tailwind utility class is introduced, regenerate the CSS with the repo's own target and commit the result; otherwise stay on existing classes.
  • Gate on make check, and run the Docker lint path via script/cibuild with the cache defeated.
`templates/source_detail.html:148-149` renders the stored target config verbatim: ``` {{if .Config}} <code class="text-xs text-gray-500 break-all block mt-1">{{.Config}}</code> ``` For a `slack` target 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 - The raw stored config blob is no longer rendered for any target type. - Each target type renders friendly, named fields instead: - `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. - Unknown or unparseable config renders a neutral placeholder, not the raw string. A target whose config fails to parse must not fall back to printing the blob. - A test asserts the rendered source detail page for a `slack` target 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 - Branch from `next`, PR based on `next`, single commit, title ending ` (closes #N)`. - Masking belongs in Go (a method on the target/config type) and not in the template, so it is unit-testable and cannot be bypassed by a future template. - Do not change the stored config format or the delivery path — this is a rendering change only. - If any new Tailwind utility class is introduced, regenerate the CSS with the repo's own target and commit the result; otherwise stay on existing classes. - Gate on `make check`, and run the Docker lint path via `script/cibuild` with the cache defeated.
clawbot added this to the 1.0.0 milestone 2026-08-11 13:56:16 +02:00
Author
Collaborator

Plan:

  • New internal/delivery/target_config_view.go: ConfigField{Label,Value} plus TargetView, a display-safe projection of database.Target that carries named config fields and no raw Config string — so a template has nothing to leak.
  • Masking is a method on the config type: (*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.
  • Fields per type: 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.
  • Unknown type, empty config, or a JSON parse failure renders a single Configuration: (unavailable) field. No raw-blob fallback on any path.
  • renderSourceDetail passes delivery.NewTargetViews(targets); templates/source_detail.html ranges the named fields using existing Tailwind classes only.
  • Tests: unit tests for the masking (including unparseable URL) and the per-type field sets, plus the load-bearing handler test that renders the real source detail page for a slack target and asserts the webhook URL's secret path segments are absent from the HTML.
Plan: - New `internal/delivery/target_config_view.go`: `ConfigField{Label,Value}` plus `TargetView`, a display-safe projection of `database.Target` that carries named config fields and **no** raw `Config` string — so a template has nothing to leak. - Masking is a method on the config type: `(*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. - Fields per type: `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. - Unknown type, empty config, or a JSON parse failure renders a single `Configuration: (unavailable)` field. No raw-blob fallback on any path. - `renderSourceDetail` passes `delivery.NewTargetViews(targets)`; `templates/source_detail.html` ranges the named fields using existing Tailwind classes only. - Tests: unit tests for the masking (including unparseable URL) and the per-type field sets, plus the load-bearing handler test that renders the real source detail page for a `slack` target and asserts the webhook URL's secret path segments are absent from the HTML.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/webhooker#113