fix(source/http): block CGNAT 100.64.0.0/10 in default SSRF guard (#3625)

The default SSRF guard in the HTTP source (`SSRFGuard.IsIPBlocked`)
decides
whether a resolved destination IP is allowed using
`!ip.IsGlobalUnicast() ||
ip.IsPrivate()`. Neither predicate covers the RFC 6598 shared address
space
`100.64.0.0/10`: those addresses are global-unicast and are not
classified as
private by `net.IP.IsPrivate` (which only knows RFC 1918 and IPv6 ULA).
So with
SSRF protection on (`allowPrivateNetworks: false`, the default), a tool
request,
or a redirect, whose target host resolves into `100.64.0.0/10` was
allowed
through.

That range is not internet-routable (RFC 6598 carrier-grade NAT / shared
address
space) and is commonly used by cloud providers and Kubernetes CNIs for
internal
node and Pod networking, so it is a real internal-reachability target
and a
standard SSRF-denylist entry. The docs already state the guard blocks
"private IP
ranges, loopback ranges, and link-local ranges (e.g. AWS/GCP metadata
service at
`169.254.169.254`)", so blocking CGNAT by default fits the stated
contract rather
than expanding it.

Impact: with the default guard, an LLM/agent-controlled tool parameter
that steers
the destination host (directly or via an HTTP redirect) into
`100.64.0.0/10` could
reach internal services on cloud/Kubernetes node and Pod networks.
`IsIPBlocked`
is the single enforcement point, reached from both the dial-time
`Control` hook and
`CheckRedirect`, so the gap applied to every HTTP-tool request.

Solution:
- Add a package-level `cgnatRange` for `100.64.0.0/10`, parsed once via
a small
`mustParseCIDR` helper (matching the file's existing
panic-on-bad-literal style).
- Extend the single default-strict predicate in `IsIPBlocked` with
  `|| cgnatRange.Contains(ip)`.
- Precedence is preserved: the `allowedIpRanges` whitelist is still
checked first
(an operator can opt a CGNAT range back in), and `allowPrivateNetworks:
true`
  still bypasses the whole default block.
- Extend `TestSSRFGuard` with range, boundary (just-below / just-above
the /10),
  `allowPrivateNetworks` bypass, and `allowedIpRanges` override cases.
- Update the HTTP source doc bullet to mention the range.

Scope note: this deliberately covers only CGNAT (`100.64.0.0/10`). The
same
predicate also does not block RFC 6890 `192.0.0.0/24`; that is left out
to keep the
change surgical and the claim tight, and can be a follow-up if
maintainers want it.

## PR Checklist

- [x] Make sure you reviewed CONTRIBUTING.md
- [ ] Make sure to open an issue as a bug/issue before writing your
code!
(No issue opened yet - this is a small, self-contained security fix. See
the

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
Co-authored-by: Wenxin Du <117315983+duwenxin99@users.noreply.github.com>
This commit is contained in:
Anas Khan
2026-07-23 00:53:26 +05:30
committed by GitHub
parent 6bf63a091f
commit a0f36f42c3
3 changed files with 49 additions and 2 deletions
+1 -1
View File
@@ -61,7 +61,7 @@ instead of hardcoding your secrets into the configuration file.
## Advanced Usage
### SSRF Protection (SSRF Guard)
By default, the HTTP source implements strict protection against Server-Side Request Forgery (SSRF) and DNS Rebinding (TOCTOU) attacks. It automatically intercepts, resolves, and blocks connection requests to private IP ranges, loopback ranges (such as `127.0.0.1`), and link-local ranges (e.g. AWS/GCP metadata service at `169.254.169.254`).
By default, the HTTP source implements strict protection against Server-Side Request Forgery (SSRF) and DNS Rebinding (TOCTOU) attacks. It automatically intercepts, resolves, and blocks connection requests to private IP ranges, loopback ranges (such as `127.0.0.1`), link-local ranges (e.g. AWS/GCP metadata service at `169.254.169.254`), and the RFC 6598 shared address space (`100.64.0.0/10`, commonly used for Kubernetes node and Pod networking).
To override the default protection or block custom ranges, configure `allowPrivateNetworks`, `allowedIpRanges`, and `customBlockedIpRanges`:
+15 -1
View File
@@ -35,6 +35,20 @@ import (
const SourceType string = "http"
const maxErrorBodyLogBytes = 1024
// cgnatRange is the RFC 6598 shared address space (100.64.0.0/10). It is not
// globally routable, so net.IP.IsPrivate reports false for it, but cloud
// providers and Kubernetes CNIs use it for internal node and Pod networking.
// The default SSRF guard treats it as private and blocks it.
var cgnatRange = mustParseCIDR("100.64.0.0/10")
func mustParseCIDR(cidr string) *net.IPNet {
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
panic(fmt.Sprintf("invalid CIDR %q: %v", cidr, err))
}
return ipNet
}
// validate interface
var _ sources.SourceConfig = Config{}
@@ -260,7 +274,7 @@ func (g *SSRFGuard) IsIPBlocked(ip net.IP) bool {
// Default strict RFC 1918 / Link-Local / Loopback protection
if !g.AllowPrivateNetworks {
if !ip.IsGlobalUnicast() || ip.IsPrivate() {
if !ip.IsGlobalUnicast() || ip.IsPrivate() || cgnatRange.Contains(ip) {
return true
}
}
+33
View File
@@ -316,6 +316,26 @@ func TestSSRFGuard(t *testing.T) {
ip: net.ParseIP("10.0.0.1"),
want: false,
},
{
desc: "CGNAT shared address space blocked",
ip: net.ParseIP("100.64.0.1"),
want: true,
},
{
desc: "CGNAT shared address space upper bound blocked",
ip: net.ParseIP("100.127.255.255"),
want: true,
},
{
desc: "IP just below CGNAT range allowed",
ip: net.ParseIP("100.63.255.255"),
want: false,
},
{
desc: "IP just above CGNAT range allowed",
ip: net.ParseIP("100.128.0.0"),
want: false,
},
}
for _, tc := range tcs {
@@ -342,6 +362,19 @@ func TestSSRFGuard(t *testing.T) {
if !guardPrivate.IsIPBlocked(net.ParseIP("192.168.1.1")) {
t.Error("expected custom blocked IP to remain blocked when AllowPrivateNetworks is true")
}
if guardPrivate.IsIPBlocked(net.ParseIP("100.64.0.1")) {
t.Error("expected CGNAT IP to be allowed when AllowPrivateNetworks is true")
}
// Test that an explicit allowedIpRanges override lets a CGNAT IP through,
// preserving the whitelist precedence over the default CGNAT block.
guardAllowCGNAT := &SSRFGuard{
AllowedRanges: mustParseCIDRs(t, []string{"100.64.0.0/10"}),
AllowPrivateNetworks: false,
}
if guardAllowCGNAT.IsIPBlocked(net.ParseIP("100.64.0.1")) {
t.Error("expected CGNAT IP to be allowed when explicitly whitelisted via AllowedRanges")
}
}
func TestSecureDialContextAndCheckRedirect(t *testing.T) {