[omp major-3] Webhook SSRF: https validation bypassable via redirects #78
Labels
No labels
actions
bug
cli
duplicate
enhancement
fork
forum
git storage
help wanted
insights
invalid
issues
moderation
oidc
ownership transfer
packages
pr/merge protection rules
projects
pull requests
question
releases
sponsorships
tags
webhooks
wiki
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
crueber/walhub#78
Loading…
Reference in a new issue
No description provided.
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?
[omp major-3] Webhook SSRF: https validation bypassable via redirects
internal/notify/webhooks.go:87-103(validateHookURLrequires https except loopback-http, but https to ANY host passes:https://127.0.0.1:PORT,169.254.169.254, RFC1918 — no IP screening on the https branch).hookClientis a defaulthttp.Clientfollowing up to 10 redirects across schemes: a validatedhttps://evil.comhook can 302 tohttp://169.254.169.254/..., defeating validation entirely. Go forwards custom headers cross-host (onlyAuthorization/Cookiestripped), soX-Walgit-Signature+ 307/308-preserved body reach the redirect target. Gated to repo admins, but that is a tenant role, not host operator.Fix
No-redirect client (
CheckRedirect— refuse or same-host-only), private/loopback IP screening at delivery time (resolve + check, reusing the repoimportisPrivateIP-class logic incl. benchmark/test ranges), or pinned resolution. Regression tests (redirect-to169.254 refused; signature never forwarded cross-host). Coverage gate holds; doc Decisions entry (law 12).Acceptance criteria
Fixed by PR #88 (#88): no-redirect delivery client + delivery-time non-public IP screening with pinned dialing in internal/events (the live sink on main); loopback stays allowed per the dev rule. Not merged — awaiting review.
PR #88 review (verified in scratch worktree at origin/fix/issue-78): SCOPE VERDICT — wrong file. internal/notify/webhooks.go EXISTS on origin/main (git ls-tree confirms) and is byte-identical on the PR branch (empty diff) — the author's claim is false and the reported hole is fully intact.
LIVE PROOF on PR branch (throwaway test, removed afterward): Hook with URL of a 302-redirecting origin + secret, via postEvent → status=200, redirect-target hits=1, X-Walgit-Signature arrived cross-host. Default hookClient still follows redirects; validateHookURL still passes https-to-any-host with no IP screen on the https branch (webhooks.go:87-103); hookClientInsecure likewise unhardened.
(a) Events-sink change (internal/events/ssrf.go + sink.go + docs/go/09_events.md): sound defense-in-depth on the OPERATOR-configured events.webhook_url — refuse-all CheckRedirect, resolve+screen+dial pinned to the screened IP literal (no TOCTOU; redirect-target dialing moot since all redirects refused), loopback preserved, stdlib-only imports, genuine redirect-trap tests asserting zero arrival, -race green, vet/gofmt clean, package coverage 97.4% (gate >=95% holds). Safe to KEEP, but it does not touch the tenant-reachable path — issue #78 must stay open.
MERGE RECOMMENDATION: blocked — needs author rework on internal/notify/webhooks.go: (1) no-redirect CheckRedirect on BOTH hookClient and hookClientInsecure (refuse-all vs same-host-only is a real trade-off for tenant webhooks behind http-to-https upgrades — pick one and justify in the doc entry); (2) delivery-time IP screening + pinned DialContext on both transports (reuse the ssrf.go logic — extract to a shared stdlib-only helper, no new imports); (3) regression tests mirroring ssrf_test.go: cross-host 302 fails delivery with zero target arrival and no signature, a 307/308 body-preserving variant, private-literal https URL refused at delivery, loopback still delivers; (4) Decisions entry in docs/features/06_notifications.md per law 12. Re-verify with go test -race plus coverage on internal/notify before re-requesting review.
Rework pushed to origin/fix/issue-78 (
4293460, on top of4b89002) — the tenant path in internal/notify/webhooks.go is now hardened. The events-sink hardening from PR #88 is KEPT (internal/events/ssrf.go keeps its names as thin wrappers, behavior unchanged).Deviations: per the review steer I extracted a NEW internal/egress package instead of duplicating ssrf.go into notify — stdlib-only so law 1 needs no amendment; it is covered by its own 97.6% suite and the cover gate. Main worktree untouched (git status identical before/after).
Not merged, no new PR — re-review requested.
Re-review of PR #88 (fix/issue-78, rework
4293460+ docs fixupcb70ecf): the hole is now CLOSED on the tenant path.What changed in the rework: the hardening moved out of the wrong file into the correct tenant lane (internal/notify/webhooks.go hookClient/hookClientInsecure) with the shared stdlib-only screen in new internal/egress, thin wrappers in internal/events/ssrf.go for the operator bridge. Both lanes now set CheckRedirect=refuse-all AND a pinned resolve-screen-dial transport.
Verified in scratch worktree /tmp/pr88b (since removed):
Fixed directly + pushed to origin/fix/issue-78 (
cb70ecf): docs/go/01_overview.md §8 package tree did NOT list the new internal/egress package (naming law: the tree is normative) — added the table row + a Decisions bullet. Re-ran gofmt/vet/tests after: all green.Notes (non-blocking): 1) Transport clones DefaultTransport so Proxy env is preserved — behind an HTTP_PROXY the dial screen sees the proxy hop, not the target; operator-controlled env, pre-existing behavior, flagging only. 2) docs/go/01_overview.md §8 and AGENTS.md §3 still lack an internal/notify row (pre-existing gap, out of this PR's scope).
MERGE RECOMMENDATION: ready to merge.
Fixed by PR #88 (rework onto tenant path + shared egress helper + docs fixup; review clean; 97.2%/98.8%/97.6% coverage), merged. Closing.