WIP: Mask the http target's destination URL in the UI (closes #115) #126
Reference in New Issue
Block a user
Delete Branch "issue-115-mask-http-destination-url"
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?
Speculative, ahead of your decision on
#115. Merging it decides
option 1; closing it decides against. Title keeps the
WIP:prefix soit cannot land by accident.
Option chosen
Option 1 — mask unconditionally, reusing the existing
MaskURLfrominternal/delivery/target_config_view.go, which rendersscheme://host/.... It is the rule already applied to the slack target,so both target types now behave identically, and it needs no new masker
and no heuristic.
Rejected:
or high-entropy path segments). A heuristic that can be wrong in both
directions: it leaks a self-hosted Mattermost or a bespoke endpoint it
does not recognise, and it hides a plain
/hookpath an operatorwanted to read.
authenticated. It keeps a written-down credential on screen, so it
survives into screenshots, screen shares and browser-rendered PDFs
regardless of who is logged in.
Correction to the issue's usability mitigation
The issue argues the cost is acceptable because "the full value stays
available in the edit form". That is not true in the code today.
internal/server/routes.goregistersPOST /source/{sourceID}/targets,.../targets/{targetID}/toggleand.../targets/{targetID}/delete—there is no target edit route, and
templates/source_edit.htmleditsonly the webhook's name, description and retention. A target's
destination URL is write-once: after this change it is not recoverable
anywhere in the UI, only from the database.
So the real cost of option 1 is higher than the issue states. It still
looks like the right call — an unrecoverable credential is the normal
outcome for a secret, and the operator holds the value at the source
(Slack, Discord, Teams) — but the decision should be made knowing that,
and a target edit form is the thing that would repay the usability debt.
Not filed as an issue: the absent edit form is your existing design, not
a defect, so it is a question for you rather than a work order.
What changed
internal/delivery/target_config_view.go—httpConfigFieldsrendersMaskURL(cfg.URL)for theDestination URLfield. No new masker; noother field changed.
Sibling-path audit
Prompted by #118, which found
this credential leaking through three sibling paths. Audited every place
target data reaches an operator:
GET /source/{id}(templates/source_detail.html)TargetView.ConfigfieldsGET /source/{id}/logs(templates/source_logs.html).Target.Nameand.StatusonlyGET /sources(templates/sources_list.html)GET /source/{id}/editDeliveryResult.Errorand loggedPOST /source/{id}/targetsSSRF rejection (response body + log line)ValidateTargetURLreports scheme/host/hostname only; theurl.Parsebranch is already run throughmaskURLError, and the log line throughMaskURL/metricsOnly the one path.
TargetViewhas no raw config field, so no templatecan reach the stored blob.
Verification
Mutation-verified. With
MaskURL(cfg.URL)reverted tocfg.URLandnothing else changed,
make testexits 2 and the new handler test failson the secret path segments at
source_detail_test.go:164:New tests:
TestHandleSourceDetail_MasksHTTPDestinationURL— the done-criterion.Seeds an
httptarget whose destination is a secret-bearing incomingwebhook URL, runs the real handler, asserts the rendered page contains
none of the path segments and does show
https://hooks.slack.com/....TestNewTargetViews_HTTPMasksDestinationURL— same rule at the viewlayer.
Gates, on the rebased head:
make checkexits 0 (tests, lint, fmt-check), re-run after the rebaseonto
nextatd19e336.docker build --no-cache-filter=lint --no-cache-filter=builder .exits 0. Not a cached no-op —
make lintexecuted in thelintstage(
0 issues.after 61s) andmake testexecuted in thebuilderstage, with every package reporting a real duration and no
(cached):ok internal/delivery 6.870s,ok internal/handlers 6.781s, and bothnew tests logging
--- PASSin that run. A scoped--no-cache-filterrather than a prune, per the shared-host rule.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.