Web Fetch DNS and SSRF Model

Purpose

web.fetch.v1 is a pre-action authorization policy. It decides whether a
proposed web request is authorized by the passport and policy context before a
harness or application performs the request.

It is not, by itself, an egress proxy, DNS resolver, or network sandbox. That
distinction matters for private-network and SSRF claims.

Current Guarantees

Hosted verifier

The hosted verifier evaluates the request metadata it receives:

  • url
  • method
  • configured allowed_domains
  • configured blocked_domains
  • headers and body size when supplied

max_requests_per_min is NOT evaluated. validateWebFetchRateLimit in
functions/utils/policy/custom-validators.ts is a stub: it returns
{ valid: true } without reading the context, with a TODO for a KV- or
D1-backed counter. A passport that sets the limit is not rate limited by this
policy, so do not rely on it as a control.

Hosted web.fetch.v1 requires an explicit domain allowlist. An empty
allowed_domains list denies. This means an arbitrary attacker-owned domain is
not authorized unless the passport explicitly allows it or uses a wildcard such
as "*".

The hosted private-network validator blocks deterministic URL host forms:

  • localhost and .localhost
  • loopback and private IPv4 literals
  • shortened, integer, octal, and hex IPv4 forms
  • IPv4-mapped IPv6 private literals
  • unique-local, link-local, unspecified, and loopback IPv6 literals

The hosted verifier does not fetch the URL, does not resolve DNS, and does not
pin the eventual outbound connection to a checked IP address. Therefore, hosted
verification is not vulnerable to APort-server SSRF from web.fetch.v1, but it
cannot prove that an allowed hostname will not resolve to a private address at
execution time.

Example:

Request Hosted decision input can detect
http://169.254.169.254/latest/meta-data Yes, private IP literal
http://127.1/admin Yes, normalized loopback literal
http://attacker.example/internal, where DNS resolves to 169.254.169.254 No, unless the runtime supplies trusted resolution evidence or APort controls the fetch path

Local bash guardrail

The local bash guardrail performs similar literal checks before allowing local
web.fetch.v1 decisions. It is intentionally small and offline-first.

As of this note, the local bash guardrail does not resolve DNS before applying
private-network checks. If a local passport has no effective domain allowlist,
or uses a broad allowlist, a hostname that resolves to a blocked private address
can bypass the literal-only private-network check.

This is a valid local guardrail gap and should be treated as security-relevant
for users who rely on local web.fetch.v1 to contain prompt-injected web
requests.

What Is Out Of Scope For A Pure Verifier

A verifier that only receives a proposed URL cannot fully guarantee DNS/IP
safety for the eventual network request. The unsafe cases include:

  • DNS records changing between authorization and fetch
  • DNS rebinding
  • split-horizon DNS
  • runtime-specific proxy and resolver behavior
  • redirects to newly resolved destinations
  • connection reuse or alternate address-family selection

Full SSRF containment requires the policy enforcement point to control or prove
the network path.

Correct High-Assurance Pattern

For strong private-network protection, use one of these patterns:

  1. APort-controlled egress proxy. The proxy resolves, checks, connects, and

records the final destination. The verifier can trust the proxy evidence.

  1. Trusted runtime evidence. The harness resolves the hostname, checks every

A/AAAA answer, passes resolved-address evidence to the verifier, and pins the
actual fetch to that checked resolution.

  1. Network sandbox. The harness or container blocks private-network egress

regardless of the verifier decision.

Best-effort DNS resolution inside the verifier can catch static DNS names that
point at private addresses, but it is not a complete TOCTOU defense and adds
latency and nondeterminism to the hot path.

For hosted deployments:

  • Keep allowed_domains narrow.
  • Avoid allowed_domains: ["*"] for agents that can read sensitive data or

act on prompt-injected web content.

  • Treat private-IP literal blocking as deterministic hardening, not complete

DNS-aware SSRF prevention.

  • Use a controlled egress proxy or sandbox when private-network egress must be

impossible.

For local guardrail deployments:

  • Configure explicit allowed_domains instead of relying on default broad

local behavior.

  • Treat broad local web.fetch access as experimental unless the harness also

provides network isolation.

  • Patch the local bash guardrail to resolve hostnames before private-network

checks as a defense-in-depth improvement.

Disclosure Classification

A report that a hostname resolving to a private address bypasses local
literal-only checks should be accepted as a real issue in the local guardrail.

Suggested classification:

  • Local bash guardrail: High when broad or absent domain allowlists are used.
  • Hosted verifier: Lower impact. Same DNS-resolution limitation exists in

private-IP semantics, but hosted verification does not perform the fetch and
hosted web.fetch.v1 requires explicit domain allowlists.

  • APort API SSRF: Not demonstrated by this finding.

Relationship To Jev-Style Tool Gates

OpenRouter's
Jev tool-call gate cookbook
describes a tool-call gate where application code sends the proposed tool call,
ticket evidence, and policy text to a Decisions model. The model returns
probabilities for yes/no questions. Application-owned thresholds turn those
probabilities into approve, block, or review.

This is directionally similar to APort because both patterns sit before the
tool call and decide whether the action should run.

They differ in important ways:

Dimension Jev-style gate APort/OAP
Identity model Application-defined state Passport-backed agent identity and capabilities
Decision shape Probabilities plus app thresholds Signed binary allow/deny decision
Policy source Policy text and task evidence supplied per gate Versioned policy packs, passport limits, and context
Audit App logs the reason and probabilities Hosted decisions can be signed, persisted, queried, and joined to runtime telemetry
Human review Middle probability range maps to review Human approval is an enforcement workflow layered around OAP decisions
Enforcement Application hook Harness, gateway, GitHub Action, middleware, or local guardrail

The useful lesson for APort is product framing: "gate the risky call, allow the
clearly safe call, block the clear violation, and only escalate ambiguous cases"
is the same buyer-friendly story. The implementation should stay OAP-native:
signed decisions, passport capabilities, policy packs, and explicit runtime
disposition instead of probabilistic output as the canonical audit record.