Skip to content

feat(outbound): gate serverless outbound requests behind an SSRF policy - #5611

Merged
MasterPtato merged 1 commit into
mainfrom
stack/feat-outbound-gate-serverless-outbound-requests-behind-an-ssrf-policy-nxxoqzqr
Sep 3, 2026
Merged

feat(outbound): gate serverless outbound requests behind an SSRF policy#5611
MasterPtato merged 1 commit into
mainfrom
stack/feat-outbound-gate-serverless-outbound-requests-behind-an-ssrf-policy-nxxoqzqr

Conversation

@MasterPtato

@MasterPtato MasterPtato commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@MasterPtato

Copy link
Copy Markdown
Contributor Author

Stack for rivet-dev/actors

Get stack: forklift get 5611
Push local edits: forklift submit
Merge when ready: forklift merge 5611

change nxxoqzqr

@railway-app

railway-app Bot commented Aug 27, 2026

Copy link
Copy Markdown

🚅 Deployed to the actors-pr-5611 environment in rivet-frontend

Service Status Web Updated
website ❌ Build Failed (View Logs) Web Sep 3, 2026 at 7:23 pm UTC
frontend-cloud 🕒 Building (View Logs) Web Sep 3, 2026 at 7:23 pm UTC
kitchen-sink 🕒 Building (View Logs) Web Sep 3, 2026 at 7:23 pm UTC
frontend-inspector 🕒 Building (View Logs) Web Sep 3, 2026 at 7:23 pm UTC
ladle 🕒 Building (View Logs) Web Sep 3, 2026 at 7:23 pm UTC
mcp-hub ✅ Success (View Logs) Web Aug 27, 2026 at 11:00 pm UTC

@railway-app
railway-app Bot temporarily deployed to rivet-frontend / actors-pr-5611 August 27, 2026 22:59 Destroyed
@MasterPtato
MasterPtato force-pushed the stack/feat-outbound-gate-serverless-outbound-requests-behind-an-ssrf-policy-nxxoqzqr branch from 3c5f349 to 4f21c76 Compare September 3, 2026 19:23
@railway-app
railway-app Bot temporarily deployed to rivet-frontend / actors-pr-5611 September 3, 2026 19:23 Destroyed
@MasterPtato
MasterPtato merged commit 4f21c76 into main Sep 3, 2026
8 of 14 checks passed
@MasterPtato
MasterPtato deleted the stack/feat-outbound-gate-serverless-outbound-requests-behind-an-ssrf-policy-nxxoqzqr branch September 3, 2026 19:23
@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Review: SSRF outbound policy for serverless requests

Solid design overall: centralizing the check in a DNS resolver + redirect-policy hook (GuardedResolver / redirect_policy in engine/packages/outbound-guard/src/client.rs) closes the classic TOCTOU/DNS-rebinding gap that a pre-connect-only check would have, and re-validating on every redirect hop is the right call. The config-write-time check in runner_config/upsert.rs plus the connect-time re-check ("re-gates configs stored before this policy existed") is a good belt-and-suspenders approach. Test coverage in outbound-guard/tests/policy.rs is thorough for the address-classification logic (private ranges, embedded-IPv4 forms, deny-over-allow precedence, credentials, scheme).

Potential issue: 0.0.0.0/8 (other than the literal 0.0.0.0) is not blocked

AddressClass::of_ipv4 (engine/packages/outbound-guard/src/policy.rs:558) only special-cases the exact unspecified address via addr.is_unspecified(). Addresses like 0.1.2.30.255.255.255 fall through every check and return None, i.e. "globally routable" → allowed by default even with allow_private_networks: false.

This is the exact class of bypass behind the "0.0.0.0 Day" SSRF research (many OS network stacks, including Linux and macOS, route the whole 0.0.0.0/8 block to the local host under certain conditions, not just the single address). Since this crate already goes out of its way to special-case IPv4-mapped/compatible/NAT64 IPv6 forms specifically to prevent smuggling a private destination past the filter, it seems worth closing this one too — e.g. treat octets()[0] == 0 as AddressClass::Unspecified (or a dedicated ThisNetwork class) rather than only the exact-zero address.

Suggestion: no test coverage for client.rs

All the tests exercise Policy directly; nothing exercises GuardedResolver, redirect_policy, or block_reason against a real (or mock) HTTP client/server. In particular, block_reason() depends on reqwest::Error's source() chain actually carrying the boxed BlockReason through from the custom DNS resolver / redirect callback — that plumbing is easy to silently break in a future reqwest upgrade or refactor, and it's exactly the part that determines whether a blocked destination gets correctly reported as ServerlessDestinationBlocked vs. a generic ServerlessConnectionError. A small integration test spinning up a local server that redirects to (or DNS-resolves to) a blocked address would cover the part unit tests on Policy can't.

Minor: Config-parameterized singletons

outbound_policy, guarded_client, and guarded_client_no_timeout in engine/packages/pools/src/reqwest.rs take a &rivet_config::Config argument but cache into process-wide OnceCells, so only the first call's config actually takes effect — later calls with a different Config silently reuse the first policy/client. Not a live bug today since each engine process loads a single config once at startup, but the function signature implies per-call configurability that doesn't exist; worth a doc comment noting the first-call-wins behavior, or asserting on it, so it doesn't bite a future caller (e.g. a test harness spinning up multiple in-process configs).

Out of scope? Unrelated files in this diff

The diff (as shown by gh pr diff) also includes examples/kitchen-sink/scripts/{large-commit-suite,residue-stats,truncate-residue}.ts, examples/kitchen-sink/src/actors/testing/large-commit-db.ts, a RIVET_OTEL_SAMPLER_RATIO removal in self-host/compose/dev-multidc-multinode/docker-compose.yml, and an outputDir path fix in self-host/compose/template/src/main.ts. None of these look related to the SSRF policy feature — worth double-checking these are intentional includes and not base-branch drift before merging.

Nits

  • engine/packages/config/src/config/outbound.rs: consistent with existing Root accessor patterns (LazyLock default), good.
  • Docs (troubleshooting.mdx, debugging.mdx) are updated in the same change, matches the docs-sync expectations in CLAUDE.md.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant