P1: blocked networks config + SSRF gaps (CGNAT, benchmark, NAT64 ranges not blocked) #67

Closed
opened 2026-08-09 03:44:32 +02:00 by clawbot · 2 comments
Collaborator

Verified against main at 61f42e6. This is the "implement blocked networks configuration to extend SSRF protection" P1 item in TODO.md; confirmed genuinely still missing — there is no such config key and no CIDR list anywhere.

The existing protection is sound in structure: validateURL (internal/httpfetcher/httpfetcher.go:322-331) resolves and checks, and ssrfSafeDialer (:412-441) resolves again and re-checks immediately before dialing, so the DNS-rebinding window is correctly closed at the dialer — that part is good and should not be regressed.

The gap is coverage of isPrivateIP (:366-409). It relies on a hardcoded list and does not appear to cover:

  • 100.64.0.0/10 — CGNAT / carrier-grade NAT, routinely used for internal infrastructure
  • 192.0.0.0/24 — IETF protocol assignments
  • 198.18.0.0/15 — benchmark/test range
  • 64:ff9b::/96 — NAT64, which maps straight back onto IPv4 private space

Uncertain, verify before changing: IPv4-mapped IPv6 (::ffff:10.0.0.1) may already be handled, because Go's ip.IsPrivate() calls To4() internally. Confirm with a test rather than assuming in either direction.

Cloud metadata endpoints (169.254.169.254) fall under link-local — confirm that is actually covered, since it is the single highest-value SSRF target in any deployment on a cloud provider.

Definition of done

  1. A blocked_networks (or similarly named, following the descriptive-naming rule) config key accepting a list of CIDRs, merged with the built-in defaults rather than replacing them. Strict parsing: an invalid CIDR aborts startup naming the key and the offending value, per repo policy.
  2. Built-in blocklist extended to cover the ranges above, in both IPv4 and IPv6 forms.
  3. Enforcement stays in ssrfSafeDialer (the authoritative check), not only in validateURL, so the rebinding protection is preserved.
  4. Failing tests first, table-driven over the address families and ranges, including: each newly blocked range, ::ffff:10.0.0.1, 169.254.169.254, and a positive case proving ordinary public addresses still resolve and dial.
  5. A test that an operator-supplied blocked_networks entry is actually enforced by the dialer.
  6. make check green.

Related

Redirect-based SSRF (the CheckRedirect validator at httpfetcher.go:133-143) is currently untested — covered in the separate httpfetcher test-coverage issue, but worth a glance while in this code.

Verified against `main` at `61f42e6`. This is the "implement blocked networks configuration to extend SSRF protection" P1 item in `TODO.md`; confirmed genuinely still missing — there is no such config key and no CIDR list anywhere. The existing protection is sound in structure: `validateURL` (`internal/httpfetcher/httpfetcher.go:322-331`) resolves and checks, and `ssrfSafeDialer` (`:412-441`) resolves **again** and re-checks immediately before dialing, so the DNS-rebinding window is correctly closed at the dialer — that part is good and should not be regressed. The gap is coverage of `isPrivateIP` (`:366-409`). It relies on a hardcoded list and does not appear to cover: - `100.64.0.0/10` — CGNAT / carrier-grade NAT, routinely used for internal infrastructure - `192.0.0.0/24` — IETF protocol assignments - `198.18.0.0/15` — benchmark/test range - `64:ff9b::/96` — NAT64, which maps straight back onto IPv4 private space **Uncertain, verify before changing:** IPv4-mapped IPv6 (`::ffff:10.0.0.1`) may already be handled, because Go's `ip.IsPrivate()` calls `To4()` internally. Confirm with a test rather than assuming in either direction. Cloud metadata endpoints (`169.254.169.254`) fall under link-local — confirm that is actually covered, since it is the single highest-value SSRF target in any deployment on a cloud provider. ## Definition of done 1. A `blocked_networks` (or similarly named, following the descriptive-naming rule) config key accepting a list of CIDRs, merged with the built-in defaults rather than replacing them. Strict parsing: an invalid CIDR aborts startup naming the key and the offending value, per repo policy. 2. Built-in blocklist extended to cover the ranges above, in both IPv4 and IPv6 forms. 3. Enforcement stays in `ssrfSafeDialer` (the authoritative check), not only in `validateURL`, so the rebinding protection is preserved. 4. Failing tests first, table-driven over the address families and ranges, including: each newly blocked range, `::ffff:10.0.0.1`, `169.254.169.254`, and a positive case proving ordinary public addresses still resolve and dial. 5. A test that an operator-supplied `blocked_networks` entry is actually enforced by the dialer. 6. `make check` green. ## Related Redirect-based SSRF (the `CheckRedirect` validator at `httpfetcher.go:133-143`) is currently untested — covered in the separate httpfetcher test-coverage issue, but worth a glance while in this code.
clawbot added this to the 1.0.0 milestone 2026-08-09 03:44:32 +02:00
Author
Collaborator

Plan for #67

  • Add a blocked_networks config key: a YAML list of CIDRs parsed with net/netip. Strict — an invalid CIDR aborts startup naming the key and the offending value. Operator prefixes are added to the built-in defaults, which stay enforced (a merge, not a replacement).
  • Extend the built-in blocklist to CGNAT 100.64.0.0/10, IETF protocol assignments 192.0.0.0/24, benchmark 198.18.0.0/15, and NAT64 64:ff9b::/96, covering IPv4-mapped-IPv6 forms.
  • Enforcement stays in the dial-time SSRF re-resolution (the authoritative check that closes the DNS-rebinding window), which now also consults the operator prefixes.
  • Failing table-driven tests first, over address families and ranges: each newly blocked range, ::ffff:10.0.0.1, 169.254.169.254, a public control, and an operator-supplied entry enforced by the dialer.
  • Document the key in README.md and config.example.yml.

Note: the issue lists 192.0.0.0/24 (IETF protocol assignments). Tests added to next today use 192.0.2.10 (TEST-NET-1, 192.0.2.0/24) as a public upstream — a disjoint range — so TEST-NET-1 stays dialable and those tests are left untouched.

Model: opus-4-8

Plan for https://git.eeqj.de/sneak/pixa/issues/67 - Add a `blocked_networks` config key: a YAML list of CIDRs parsed with `net/netip`. Strict — an invalid CIDR aborts startup naming the key and the offending value. Operator prefixes are added to the built-in defaults, which stay enforced (a merge, not a replacement). - Extend the built-in blocklist to CGNAT `100.64.0.0/10`, IETF protocol assignments `192.0.0.0/24`, benchmark `198.18.0.0/15`, and NAT64 `64:ff9b::/96`, covering IPv4-mapped-IPv6 forms. - Enforcement stays in the dial-time SSRF re-resolution (the authoritative check that closes the DNS-rebinding window), which now also consults the operator prefixes. - Failing table-driven tests first, over address families and ranges: each newly blocked range, `::ffff:10.0.0.1`, `169.254.169.254`, a public control, and an operator-supplied entry enforced by the dialer. - Document the key in `README.md` and `config.example.yml`. Note: the issue lists `192.0.0.0/24` (IETF protocol assignments). Tests added to `next` today use `192.0.2.10` (TEST-NET-1, `192.0.2.0/24`) as a public upstream — a disjoint range — so TEST-NET-1 stays dialable and those tests are left untouched. Model: opus-4-8
Author
Collaborator

Implemented in #124 (base next).

Added a blocked_networks config key: a list of CIDRs parsed with net/netip, added to the built-in SSRF blocklist rather than replacing it; an invalid CIDR aborts startup naming the key and the offending value. The built-in ranges now also cover CGNAT 100.64.0.0/10, IETF protocol assignments 192.0.0.0/24, benchmark 198.18.0.0/15, and NAT64 64:ff9b::/96, with IPv4-mapped-IPv6 forms unmapped before matching. Enforcement stays in the dial-time re-resolution, which now also consults the operator-supplied prefixes. Documented in README.md and config.example.yml.

On the range you flagged as uncertain: ::ffff:10.0.0.1 is blocked (a test pins it), and 169.254.169.254 is already covered by the link-local check (also pinned).

Judgement call: the issue lists 192.0.0.0/24, while the Fetch tests added to next today use 192.0.2.10 (TEST-NET-1, 192.0.2.0/24) as a public upstream. Those are disjoint ranges, so TEST-NET-1 stays dialable and those tests were left unchanged.

Model: opus-4-8

Implemented in https://git.eeqj.de/sneak/pixa/pulls/124 (base `next`). Added a `blocked_networks` config key: a list of CIDRs parsed with `net/netip`, added to the built-in SSRF blocklist rather than replacing it; an invalid CIDR aborts startup naming the key and the offending value. The built-in ranges now also cover CGNAT `100.64.0.0/10`, IETF protocol assignments `192.0.0.0/24`, benchmark `198.18.0.0/15`, and NAT64 `64:ff9b::/96`, with IPv4-mapped-IPv6 forms unmapped before matching. Enforcement stays in the dial-time re-resolution, which now also consults the operator-supplied prefixes. Documented in `README.md` and `config.example.yml`. On the range you flagged as uncertain: `::ffff:10.0.0.1` is blocked (a test pins it), and `169.254.169.254` is already covered by the link-local check (also pinned). Judgement call: the issue lists `192.0.0.0/24`, while the Fetch tests added to `next` today use `192.0.2.10` (TEST-NET-1, `192.0.2.0/24`) as a public upstream. Those are disjoint ranges, so TEST-NET-1 stays dialable and those tests were left unchanged. Model: opus-4-8
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/pixa#67