fix(web): separate net_private opt-in from the net reachability allowlist (#270/AB-007) - #314
Open
hartsock wants to merge 1 commit into
Open
fix(web): separate net_private opt-in from the net reachability allowlist (#270/AB-007)#314hartsock wants to merge 1 commit into
hartsock wants to merge 1 commit into
Conversation
…list (#270/AB-007) WHAT Split the web tool's two conflated authorities. `net` (the effective caveat) now governs host *reachability* only. A new, separate `net_private: Scope<String>` — construction-time tool config, default `Scope::none()` — governs which hosts may resolve into private / loopback / link-local / unique-local / metadata space. `screen_host` takes both; the private-space opt-in is read from `net_private`, never from `net` membership. `WebFetchTool::with_private_hosts(..)` sets the escape hatch; `new()`/`default()` block all private-address resolution. WHY AB-007 (adversarial audit, risk:high): a hostname on the `net` allowlist was *also* the opt-in for that host to resolve to private/loopback IPs. So every explicitly-allowed public host was an implicit SSRF / private-space grant — a compromised, split-horizon, or rebinding DNS answer of 127.0.0.1, 10.0.0.0/8, 169.254.169.254, or fc00::/7 was accepted because the *hostname* was allowlisted. Reachability and crossing the public/private boundary are distinct authorities and are now screened against distinct inputs. A public-host grant keeps private-address blocking ENABLED. Secure by default: the facade `registry()` builds `WebFetchTool::new()`, so the default tool opts no host into private space. A host that needs local/internal access constructs the tool with `with_private_hosts(...)`. Layering: kept below the Caveats law line (owner decision) — `net_private` is web-tool config, not a new protocol axis, so no agent-mesh-protocol change or crate release is required. It can be promoted to a per-grant capability axis later if per-dispatch delegation of private space is needed. Regression tests (fail before, pass after): - unit: an allowlisted public host resolving to loopback/metadata stays denied; a mixed public/private answer keeps only the public address; a net_private opt-in is host-specific and does not leak; both-axes escape hatch still works. - integration: the default tool SSRF-denies a loopback host that is on the net allowlist (before #270 this connected); legit loopback tests now use with_private_hosts. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
AB-007 (audit, risk:high): a public-host allowlist also granted private/loopback resolution (SSRF). In the web tool, a hostname on the
netallowlist was also the opt-in for that host to resolve to private IPs — so every explicitly-allowed public host was an implicit SSRF / private-space grant. A compromised / split-horizon / rebinding DNS answer of127.0.0.1,10.0.0.0/8,169.254.169.254, orfc00::/7was accepted because the hostname was allowlisted.This splits the two conflated authorities:
net(the effective caveat) governs host reachability only.net_private: Scope<String>— a separate opt-in, defaultScope::none()— governs which hosts may resolve into private / loopback / link-local / unique-local / metadata space.screen_host(net, net_private, host, resolved)reads the private-space opt-in fromnet_private, never fromnetmembership. A public-host grant keeps private-address blocking enabled.Secure by default
The facade
registry()buildsWebFetchTool::new()(net_private = none), so the default tool opts no host into private space. A host that needs local/internal access constructsWebFetchTool::with_private_hosts(Scope::only([...]))— the explicit escape hatch.Layering (per your "web-tool mechanism" choice)
Kept below the Caveats law line:
net_privateis web-tool config, not a newagent-mesh-protocolaxis — so no protocol change and no crate release. If per-dispatch delegation of private space is ever needed, it can be promoted to a capability axis later.Test plan
Green locally:
cargo test -p agent-bridle-tool-web --features web(28 unit + 5 integration),cargo fmt --check,cargo clippy --features web -D warnings(web + umbrella), and the pre-push Lean formal gate (157 theorems within[propext, Quot.sound]).Regression tests (fail on the pre-#270 code, pass now):
net_guard.rs):ab007_allowlisted_public_host_stays_ssrf_blocked,ab007_cloud_metadata_blocked_for_allowlisted_public_host,ab007_allowlisted_public_host_mixed_answer_drops_private_keeps_public,net_private_optin_is_host_specific,net_private_optin_allows_loopback_for_the_named_host.net_leash.rs):ab007_net_allowlist_alone_does_not_open_private_space— the default tool SSRF-denies a loopback host that is on thenetallowlist (before AB-007: public-host allowlist also grants private/loopback resolution (SSRF) #270 it connected). Legit loopback tests now usewith_private_hosts.Out of scope
net_privateto a per-grantagent-mesh-protocolCaveats axis (the capability path) — deferred; would require a protocol release.Fixes #270
Note
High Risk
This changes the SSRF enforcement boundary for
web_fetch; misconfiguration ofnet_privateor callers still assumingnetimplies private-space access could break legitimate local fetches or leave a gap if hosts are not migrated towith_private_hosts.Overview
Fixes AB-007 (#270): a host on the session
netgrant no longer doubles as permission to connect to private, loopback, link-local, or cloud-metadata addresses after DNS/rebinding.netstill gates whether a hostname may be fetched;net_privateis new tool construction config (defaultScope::none()).screen_hostnow takes both scopes, and private-space checks usehost_may_reach_private_spaceonnet_privateonly—not membership innet. Loopback or internal testing usesWebFetchTool::with_private_hosts(...)in addition to a matchingnetgrant.Public exports drop
host_is_explicitly_allowlistedin favor ofhost_may_reach_private_spaceand the updatedscreen_host. Unit and integration tests lock in the regression (allowlisted public host → loopback/metadata denied; mixed DNS answers drop private IPs).Reviewed by Cursor Bugbot for commit f5d69a3. Configure here.