Skip to content

pkg/httpclient: extend SSRF block-list (loopback, RFC 1918, all link-local, hostname/DNS resolution) #20

Description

@cristim

Context

Spun out of the adversarial review of PR LeanerCloud/cloud-commitments-cli#1231 (SEC-04, hardened HTTP client for JWKS warmup and fetch). The PR moves the existing Azure IMDS-blocking implementation into pkg/httpclient and applies it to the scheduled-task OIDC validator. PR LeanerCloud/cloud-commitments-cli#1231 also picked up a parity fix (canonical/expanded IPv6 form comparison via net.IP.Equal). The gaps below are larger and were carved out as follow-ups.

Gaps in the current block-list

pkg/httpclient.blockIMDSDialer.DialContext only rejects the dial when the literal IP in the URL parses to the AWS IMDS IPv4 or IPv6 endpoint. Several SSRF-adjacent paths are still reachable:

  1. Loopback (127.0.0.0/8, ::1): a JWKS URL pointed at http://127.0.0.1:6379/ or http://localhost:8080/ reaches whatever loopback services run on the same host (Redis, Prometheus, sidecar admin endpoints).
  2. RFC 1918 private space (10/8, 172.16/12, 192.168/16): intranet pivot.
  3. All link-local (169.254.0.0/16): only 169.254.169.254 is blocked. Sibling link-local addresses are also reachable.
  4. Other IPv6 link-local (fe80::/10): not blocked.
  5. Hostname-based SSRF: a URL whose hostname is metadata.google.internal, instance-data.ec2.internal, etc., or a hostile DNS record that resolves to a metadata IP, bypasses the block-list. The current dialer only inspects the literal host string off net.SplitHostPort and does not look at the resolved IP.

(1) through (4) follow the standard SSRF deny-list pattern.
(5) requires resolving the hostname before the connect, validating the resolved IP against the deny-ranges, and dialing the specific IP (so a DNS-rebinding attack cannot swap the IP between resolution and connect).

Fix sketch

In pkg/httpclient:

  • Replace the IMDS list with a set of denied net.IPNet ranges covering link-local, loopback, RFC 1918, multicast, unspecified, and IPv6 site-local/ULA-metadata.
  • Wrap DialContext so it pre-resolves the host (via net.DefaultResolver.LookupNetIP with the dial ctx), rejects any resolved IP that falls in a denied range, and dials the chosen IP literal (preventing DNS rebinding between resolution and connect).
  • Keep a per-call test seam (resolver injection) so the deny-range behavior can be asserted in unit tests without going through real DNS.

Test coverage

  • Each denied range gets its own positive test (127.0.0.1, 127.0.0.2, 10.0.0.1, 192.168.1.1, 169.254.1.1, fe80::1, ::1).
  • Each denied range gets a hostname-shaped variant via a stub resolver that returns the denied IP.
  • Negative tests for non-denied public IPs and hostnames.

Closing-issue trailer for the fix PR

Closes #<this issue number>

Findings from the 2026-09-02 codebase audit

Added by an automated audit of 3c0f8ac94048a2c36fce5ccddee54e6c4849a5cd (tip of origin/main). Each item below was reported by one reviewer and independently confirmed by a second that did not write it. Full report: docs/audits/codebase-audit-2026-09-02.md.

A08b-002 (high)

Two addresses to add to the deny-set in this issue, from finding A08b-002. The ranges listed here (loopback, RFC 1918, 169.254.0.0/16, fe80::/10) do not cover Azure's WireServer / host-agent endpoint 168.63.129.16, which serves instance configuration and extension settings and is a plain routable address, nor Alibaba's 100.100.100.200. http://168.63.129.16/machine?comp=goalstate is reachable today from every Azure service client, all of which carry a managed-identity token. Worth adding both as explicit single-address entries alongside the CIDR set, since a range-only deny-list will not catch them.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions