Skip to content

fix(web): separate net_private opt-in from the net reachability allowlist (#270/AB-007) - #314

Open
hartsock wants to merge 1 commit into
mainfrom
fix/ab-007-net-private-web-guard
Open

fix(web): separate net_private opt-in from the net reachability allowlist (#270/AB-007)#314
hartsock wants to merge 1 commit into
mainfrom
fix/ab-007-net-private-web-guard

Conversation

@hartsock

@hartsock hartsock commented Jul 29, 2026

Copy link
Copy Markdown
Member

Summary

AB-007 (audit, risk:high): a public-host allowlist also granted private/loopback resolution (SSRF). In the web tool, a hostname on the net allowlist 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 of 127.0.0.1, 10.0.0.0/8, 169.254.169.254, or fc00::/7 was 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, default Scope::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 from net_private, never from net membership. A public-host grant keeps private-address blocking enabled.

Secure by default

The facade registry() builds WebFetchTool::new() (net_private = none), so the default tool opts no host into private space. A host that needs local/internal access constructs WebFetchTool::with_private_hosts(Scope::only([...])) — the explicit escape hatch.

Layering (per your "web-tool mechanism" choice)

Kept below the Caveats law line: net_private is web-tool config, not a new agent-mesh-protocol axis — 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):

  • unit (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.
  • integration (net_leash.rs): ab007_net_allowlist_alone_does_not_open_private_space — the default tool SSRF-denies a loopback host that is on the net allowlist (before AB-007: public-host allowlist also grants private/loopback resolution (SSRF) #270 it connected). Legit loopback tests now use with_private_hosts.

Out of scope

  • Promoting net_private to a per-grant agent-mesh-protocol Caveats axis (the capability path) — deferred; would require a protocol release.
  • Re-resolve-per-redirect / mixed-answer policy knobs beyond the current drop-private-keep-public behavior.

Fixes #270


Note

High Risk
This changes the SSRF enforcement boundary for web_fetch; misconfiguration of net_private or callers still assuming net implies private-space access could break legitimate local fetches or leave a gap if hosts are not migrated to with_private_hosts.

Overview
Fixes AB-007 (#270): a host on the session net grant no longer doubles as permission to connect to private, loopback, link-local, or cloud-metadata addresses after DNS/rebinding.

net still gates whether a hostname may be fetched; net_private is new tool construction config (default Scope::none()). screen_host now takes both scopes, and private-space checks use host_may_reach_private_space on net_private only—not membership in net. Loopback or internal testing uses WebFetchTool::with_private_hosts(...) in addition to a matching net grant.

Public exports drop host_is_explicitly_allowlisted in favor of host_may_reach_private_space and the updated screen_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.

…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]>
@hartsock hartsock added the risk:high High-risk change label Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:high High-risk change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AB-007: public-host allowlist also grants private/loopback resolution (SSRF)

1 participant