Mask the target URL in delivery errors, SSRF logs and log page data (closes #118) #121
Reference in New Issue
Block a user
Delete Branch "issue-118-mask-credential-leaks"
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?
Closes #118.
A delivery target URL is itself a credential — a Slack incoming webhook URL is a bearer token — and three paths still reproduced it in full.
Delivery errors (the one that wrote it to disk)
net/httpembeds the request URL in every*url.Errorit returns, so any DNS, TLS, timeout or dial failure stored the whole webhook URL inDeliveryResult.Error: on disk, in the per-webhook database, behind ajson:"error,omitempty"tag that a REST API would serialize.maskURLmoves tointernal/delivery/url_mask.goand is exported asMaskURL(the handlers need it);maskURLErrorjoins it there. It rebuilds the*url.ErrorwithURLreplaced byMaskURL(...), keepingOpand the wrapped cause, so the diagnostic survives — the class of failure and the host are still reported, anderrors.Is/As/Timeout/Temporarystill work through it. Only path, query and userinfo are dropped:It is applied where the errors are raised rather than where they are stored:
executeHTTPRequest(shared by the Slack and HTTP targets) and the two request-construction paths. Everything downstream is then safe by construction.url.Parseembeds the URL in its error as well, soValidateTargetURL's parse branch gets the same treatment — that error is both logged and shown.SSRF log and the event log page
The SSRF rejection warning logs
MaskURL(targetURL).loadTargetMapnow returnsmap[string]delivery.TargetView, andEventWithDeliveries.Deliveriesis a[]DeliveryView(id, status,TargetView), sosource_logs.htmlhas no reachable path to a stored config blob. The page renders exactly as before — it uses.Target.Nameand.Status.Tests
TestDeliverSlack_TransportErrorMasksWebhookURLis the load-bearing one: it asserts the storedDeliveryResult.Errorcontains neither the webhook path nor any single segment of it, while still containingsending request,Post, the masked host andconnection refused. Verified to bite — with the mask removed fromexecuteHTTPRequestit fails on all five segments and on the missing masked host:Also covers the unparsable-URL paths (Slack attempt and
ValidateTargetURL), the HTTP target's transport error, and the rendered event log page.Verification
make check: green. Docker path with the cache defeated (docker build --no-cache-filter=lint,builder --progress=plain .), run after the rebase onto currentnext: the lint stage executedmake lint(0 issues.) and the builder stage executedmake test— every package reports a real duration, none(cached);internal/delivery11.038s,internal/handlers9.922s.PASS — all three leaks in #118 are closed, one masker in the tree, the load-bearing test bites, retry/backoff is unaffected (it keys on
attemptResult.success, never on error type), and the mask fails closed on empty/unparseable/userinfo/query/fragment URLs whileerrors.Is/As/Timeout/Temporary/Opall survive the rebuild.Disclosures:
b6529f4— the status ispending/ "Waiting to run", created 14:57 and never started. This is not specific to this PR: the basenexthead84b758bis equally stuck, and recentmaincommits (d51cd0f,4f5ecb1) report "Has been cancelled". The runner appears down repo-wide. I therefore executed the gate myself instead of failing onneeds-checks:docker build --no-cache-filter=lint,builder --progress=plain ., exit 0,#21 [lint 8/8] RUN make lint->69.93 0 issues./#21 DONE 70.8s, and#28 [builder 8/10] RUN make test->#28 DONE 84.4swith every package reporting a real duration and no(cached)marker (internal/delivery 5.435s,internal/handlers 5.357s). The four masking tests are visible as=== RUNin that output. Verdict is on that evidence, not on the tracker's status badge.maskURLError(internal/delivery/url_mask.go:50) useserrors.As, so if the*url.Erroris ever not the outermost error the returned value replaces the whole chain and any outer context is dropped. Confirmed by probe. It is documented in the function's own comment and no current call site wraps before masking, so this is a note for future call sites, not a defect.TestHandleSourceLogs_MasksSlackWebhookURL(internal/handlers/source_logs_test.go:104) does not bite the leak-3 change:templates/source_logs.htmlrenders only.Target.Nameand.Status, so itsNotContainsassertions would also pass against the pre-fix[]database.Delivery. Leak 3 is structural and the type change satisfies it; the test's real value is itsContains(tgt.Name)/Contains("delivered")guard proving the page is not blanked, which it does.