Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dbdcbe0746
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| a == 0x2001 and b == 0x0DB8 -> true | ||
| # Embedded IPv4 forms: unwrap and re-run the v4 classifier. | ||
| embedded_v4(a, b, c, d, e, f, g, h) != nil -> blocked_embedded(a, b, c, d, e, f, g, h) | ||
| true -> false |
There was a problem hiding this comment.
Reject all non-global IPv6 ranges
Reject IPv6 addresses outside globally routable space rather than allowing every prefix not explicitly listed. On networks routing deprecated site-local space such as fec0::/10, or the local-use NAT64 prefix 64:ff9b:1::/48, these addresses reach this fallback as allowed; a URL resolving to one can therefore make the Slack or webhook worker dial an internal destination and bypass the SSRF guard.
Useful? React with 👍 / 👎.
| defp build_target(uri = %URI{scheme: scheme, port: port}, host, ip) do | ||
| %Target{ | ||
| ip: ip, | ||
| url: build_request_url(scheme, ip_literal(ip), port, uri.path, uri.query), | ||
| host_header: build_host_header(scheme, host, port), |
There was a problem hiding this comment.
Preserve URL userinfo when pinning the target
Preserve uri.userinfo when rebuilding the pinned URL. For a previously accepted webhook such as https://user:password@example.com/hook, HTTPoison used the userinfo for HTTP Basic authentication, but the new target becomes https://<ip>:443/hook; both webhook and Slack requests consequently lose their credentials and receive authentication failures.
Useful? React with 👍 / 👎.
|
The egress guard is good work and the core design holds up — resolve-then-pin genuinely closes the DNS-rebinding window, rejecting when any resolved address is non-public is the right strictness, and the blocked/allowed test matrix is thorough. Four things before it should merge, one of them a silent production regression. 1. Please split this PR. The title is 2. Blocking — URL userinfo is dropped, which silently breaks basic-auth webhooks.
The failure is also invisible to us. Nothing rejects such URLs today either: Suggested fix: add 3. Pin the hackney floor, or set the TLS options explicitly.
Right now that's fine only because this PR happens to bump hackney to 1.25.0. Nothing enforces it: hackney is transitive via httpoison and undeclared in 4. Invert the IPv6 classifier to allow-only-global.
Things I checked that are correct, so they don't need re-litigating: the |
…ed advisories hackney is a direct dep. Bump to ~> 1.25 (via mix deps.get) clears GHSA-9fm9-hp7p-53mf and GHSA-vq52-99r9-h5pw. Remaining 4 advisories need hackney 4.x, which no httpoison release supports; allowlisted individually in .mix-audit.txt, tracked in renderedtext/tasks#10462. Note: notifications sends outbound HTTP to customer-configured webhook/Slack URLs, so the not-reachable reasoning other services use for these 4 GHSAs does not apply here; recorded as accepted risk instead, worth a second look given a prior commit on this branch left them deliberately unallowlisted for that reason.
…tries, restore the deliberate no-suppress decision (090d621); keep hackney 1.25.0 bump
…rders Validate outbound notification URLs before issuing the request. Reject non-http(s) schemes and control bytes, percent-decode and resolve the destination host across IPv4 and IPv6, and block loopback, link-local, private, CGNAT, reserved, and cloud-metadata addresses, including IPv4 addresses embedded in IPv6. Any resolved private address blocks the whole request and resolution failure fails closed; public destinations are unaffected. Disable redirect following on the Slack forwarder so a redirect cannot bounce to a private host.
Harden the outbound egress guard for webhook and Slack forwarders. - Resolve and classify the destination once, then dial the vetted public IP directly so the HTTP client cannot re-resolve the hostname to a different (internal) address at connect time. For https, keep TLS SNI and certificate hostname verification bound to the original hostname against the trusted CA bundle, so legitimate customer webhooks still verify while a rebind to an internal address is never dialed. - Classify IPv4-compatible IPv6 (the deprecated ::/96 form) as non-public so wrappers such as ::169.254.169.254 and ::127.0.0.1 are blocked. - Fail closed on invalid UTF-8 by returning an error instead of raising, so the blocked metric and log still fire. - Bound each DNS lookup with a timeout and treat a timeout as unresolvable. - Verify the Slack URL once before fanning out across channels.
…ail-closed tests Keep the IPv4/IPv6 classifier functions as single auditable units with an inline CyclomaticComplexity disable instead of splitting a security-critical allow/deny table, and align the three parameter patterns with the project's variable-before-pattern style. While here, fold in the small hardening the guard was missing: block IPv6 multicast (ff00::/8), strip a trailing FQDN dot before resolve and TLS binding so normalization matches hackney, and bound injected resolvers so a hung resolver fails closed. Cover the resolver timeout and the TLS SNI / hostname binding with tests.
The four remaining hackney advisories have no fix at a version this service can adopt (fixed in 4.0.1, which needs an httpoison major bump). Each is now suppressed with the application-layer mitigation that neutralizes it in this service: Notifications.Egress.UrlGuard control-byte reject, scheme allowlist, and DNS-resolve plus public-IP pinning ahead of every webhook/Slack request. The 1.25.0 bump already fixes the other two former findings (GHSA-vq52-99r9-h5pw, GHSA-9fm9-hp7p-53mf), so they no longer fire and are dropped from the note rather than suppressed.
dbdcbe0 to
cce3ba5
Compare
No description provided.