{"schema":"threatline.record/2","record":{"id":"GHSA-whx4-hxwq-qgjh","aliases":["GHSA-whx4-hxwq-qgjh"],"summary":"SSRF via DNS Rebinding in Webhook and Custom OAuth Provider URL Validation","details":"## Summary\r\n\r\nFider validates admin-supplied webhook URLs and custom OAuth provider profile/token URLs by resolving the hostname and checking that the resolved IP is not private/internal (`validate.WebhookURL`). This validation happens as a separate DNS lookup from the actual outgoing HTTP request made later (when the webhook fires, or when the OAuth profile is fetched during login), which performs its own independent DNS resolution via the shared `http.DefaultClient`. An attacker who controls DNS for the domain used in the webhook/OAuth URL can answer the validation lookup with a public IP and the connection lookup with a private/internal one (DNS rebinding), completely bypassing the intended SSRF protection.\r\n\r\nAffected versions: confirmed against the current default branch as of 2026-09-16.\r\n\r\n## Details\r\n\r\n`app/pkg/validate/general.go`'s `WebhookURL`:\r\n\r\n```go\r\nfunc WebhookURL(rawurl string) []string {\r\n    ...\r\n    addrs, err := net.LookupHost(hostname)\r\n    ...\r\n    for _, addr := range addrs {\r\n        if ip := net.ParseIP(addr); ip != nil && isBlockedIP(ip) {\r\n            return []string{\"This URL is not allowed because it targets a private or internal network address.\"}\r\n        }\r\n    }\r\n    return []string{}\r\n}\r\n```\r\n\r\nCalled at webhook creation time and webhook trigger time (`services/webhook/webhook.go`), and for custom OAuth provider `TokenURL`/`ProfileURL` (`services/oauth/oauth.go`).\r\n\r\nThe actual outgoing request goes through `app/services/httpclient/httpclient.go`'s `requestHandler`, which (before the fix) always used the process-wide `http.DefaultClient` with no further IP validation:\r\n\r\n```go\r\nres, err := http.DefaultClient.Do(req)\r\n```\r\n\r\n`http.DefaultClient`'s transport resolves the hostname itself when it dials the TCP connection, independently of `validate.WebhookURL`'s earlier lookup. An attacker-controlled authoritative DNS server with a low TTL can return a public IP for the validation query and a private/internal IP (e.g. `169.254.169.254`, `127.0.0.1`, an internal service) for the connection query moments later.\r\n\r\n## PoC\r\n\r\n```go\r\nserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\r\n    w.Write([]byte(\"internal-response-should-never-be-seen\"))\r\n}))\r\ndefer server.Close()\r\n\r\nreq, _ := http.NewRequest(\"GET\", server.URL, nil) // resolves to 127.0.0.1\r\nres, err := http.DefaultClient.Do(req)\r\n// err == nil, res.StatusCode == 200: connection to a loopback address succeeds,\r\n// regardless of any earlier, separate validation of the same hostname\r\n```\r\n\r\nI validated the fix with two Go tests exercising the real `requestHandler` function directly:\r\n- With the new `BlockPrivateNetworkTargets` flag unset (matching every other current caller): request to a loopback target succeeds (documents the vulnerable behavior).\r\n- With the flag set (as the fix now sets on the webhook trigger and OAuth-profile-fetch call sites): the identical request is blocked, since the fix re-validates the exact IP at the moment of dialing rather than relying on an earlier separate lookup.\r\n\r\nAll pre-existing `validate`/`httpclient` tests pass, and `go build ./...` succeeds project-wide, confirming no regression to the other unrelated callers of `cmd.HTTPRequest` (userlist sync, Mailgun, Gravatar, OAuth token exchange).\r\n\r\n## Impact\r\n\r\nAn account with permission to create webhooks or configure a custom OAuth provider (administrator role) can cause the Fider server to make outbound HTTP requests to arbitrary internal network addresses via DNS rebinding, bypassing the application's own explicit SSRF protection. In cloud deployments this can reach the instance metadata service (potentially exposing IAM credentials) or other internal services not otherwise exposed to that admin's trust level.\r\n\r\n## Suggested fix\r\n\r\nRe-validate the exact IP address at the moment the connection is dialed, using a custom `Transport.DialContext` that performs its own resolution and IP check immediately before connecting, so no separate earlier lookup's result can be stale. I have a working patch (adds an opt-in `BlockPrivateNetworkTargets` field to `cmd.HTTPRequest`, set only on the webhook-trigger and OAuth-profile-fetch call sites, respecting the existing `AllowPrivateNetworkTargets` escape hatch) and regression tests, happy to open a PR against a private fork if useful.\r\n","categories":["Unclassified"],"product":"github.com/getfider/fider","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/getfider/fider/security/advisories/GHSA-whx4-hxwq-qgjh","source_modified_at":"2026-10-01T19:49:55Z","fetched_at":"2026-10-28T08:18:35.509402Z","package":{"ecosystem":"go","name":"github.com/getfider/fider"},"range":"<= 0.37.0"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/getfider/fider/security/advisories/GHSA-whx4-hxwq-qgjh","source_modified_at":"2026-10-01T19:49:55Z","fetched_at":"2026-10-28T08:18:35.509402Z","package":{"ecosystem":"go","name":"github.com/getfider/fider"},"version":"0.38.0"}],"cvss":7.1,"severity":"High","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-09-28T19:02:57Z","modified_at":"2026-10-01T19:49:55Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/getfider/fider/security/advisories/GHSA-whx4-hxwq-qgjh","fetched_at":"2026-10-28T08:18:35.509402Z","source_modified_at":"2026-10-01T19:49:55Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-10-01T19:49:55Z","content_hash":"a5e41e9a6d56ac9f72f89c6266a233cc46d47e368cdbee7d31c81826108a317f"}}
