Files
goclaw/internal/tools/web_shared_ssrf_allowlist_test.go
T
Conner MoandConner Mo c21499a7f5 feat(security): let operators un-block CIDRs behind a transparent proxy (#1465)
SSRF protection resolves a hostname and judges the resulting IP. That
model assumes DNS resolution describes where the traffic actually goes,
which stops being true behind a TUN/fake-IP proxy: every query is answered
with a synthetic address out of a reserved range, and the proxy then
routes that address to the real public host. The IP is a handle, not a
destination.

In that environment web_fetch rejects ordinary public sites — observed
with news.sina.cn resolving to 198.18.0.236 — and no configuration can
fix it, because the block list is compiled in. The agent then burns
iterations retrying URLs that can never succeed.

GOCLAW_SSRF_ALLOWED_CIDRS lets an operator name the ranges their proxy
hands out. Empty by default, so nothing changes for deployments that do
not set it, and the accepted and refused entries are both logged at
startup — this widens what LLM- and admin-supplied URLs can reach, so it
should be visible.

Ranges an SSRF actually targets can never be allowlisted: link-local
(including cloud metadata at 169.254.169.254), multicast and unspecified
are refused at parse time, in either direction, so neither an exact entry
nor a wider range that swallows one gets through.

Applied inside isBlocked rather than only in validate() because
NewSafeClient re-checks the pinned IP at dial time through the same
function — relaxing just the pre-flight check would pass validation and
then fail to connect.

internal/tools carries its own private-range list for web_fetch and
web_search, separate from this package and not identical to it. It has to
consult the same setting, or relaxing one gate leaves the other rejecting
the very traffic the operator permitted. Unifying the two lists is left
alone here; it is a wider change than this one.

Co-authored-by: Conner Mo <connermo@ConnerdeMacBook-Pro.local>
2026-07-31 21:30:01 +07:00

47 lines
1.5 KiB
Go

package tools
import (
"testing"
"github.com/nextlevelbuilder/goclaw/internal/security"
)
// withOperatorAllowlist installs an operator allowlist for the duration of t.
func withOperatorAllowlist(t *testing.T, spec string) {
t.Helper()
t.Cleanup(security.SetOperatorAllowlistForTest(spec))
}
// internal/tools carries its own private-range list, separate from
// internal/security. The operator allowlist has to reach both: relaxing only
// the security package left web_fetch rejecting exactly the addresses the
// operator had just permitted.
func TestCheckSSRF_HonorsOperatorAllowlist(t *testing.T) {
// 198.18.0.0/15 is in the local list, so with no allowlist it must fail.
if err := CheckSSRF("https://198.18.0.236/"); err == nil {
t.Fatal("198.18.0.236 accepted with no allowlist configured")
}
withOperatorAllowlist(t, "198.18.0.0/15")
if err := CheckSSRF("https://198.18.0.236/"); err != nil {
t.Fatalf("allowlisted address still rejected by CheckSSRF: %v", err)
}
}
// Whatever the operator allowlists, the ranges an SSRF actually targets stay
// unreachable.
func TestCheckSSRF_KeepsProtectedRangesBlocked(t *testing.T) {
withOperatorAllowlist(t, "198.18.0.0/15")
for _, target := range []string{
"https://169.254.169.254/latest/meta-data/", // cloud metadata
"https://10.0.0.1/", // RFC 1918
"https://127.0.0.1/", // loopback
} {
if err := CheckSSRF(target); err == nil {
t.Fatalf("%s was accepted while only 198.18.0.0/15 was allowlisted", target)
}
}
}