Skip to content

Cache DNS guard resolutions and harden the outbound URL guard - #1524

Merged
RhysSullivan merged 13 commits into
mainfrom
fix/hosted-dns-guard-latency
Aug 4, 2026
Merged

Cache DNS guard resolutions and harden the outbound URL guard#1524
RhysSullivan merged 13 commits into
mainfrom
fix/hosted-dns-guard-latency

Conversation

@RhysSullivan

Copy link
Copy Markdown
Collaborator

Self-hosted proxying took 5-7s per request; another MCP server through the same tunnel answered in ~1s.

Every outbound request ran an uncached getaddrinfo in the SSRF guard before the fetch, and again for each redirect hop. On a network whose resolver drops AAAA queries — common behind IPv4-only home routers — that lookup stalls for the resolver timeout. Reproduced in a container with a stub resolver that answers A and drops AAAA: 6-9s on every call.

Guard resolutions are now cached per hostname (60s TTL, 256 entries, per guarded-fetch adapter). Failures aren't retained, concurrent callers share one in-flight resolution, and the lookup runs on a detached fiber so one caller's abort can't interrupt the resolution others are waiting on. Same repro after: 2022ms, then 1ms, 0ms.

The cache made a few pre-existing guard weaknesses load-bearing, so they're fixed here too:

  • [::169.254.169.254] was allowed — classification read the leading group of a compressed literal rather than expanding to eight words. Now handles v4-mapped, v4-compatible and NAT64 prefixes by their embedded IPv4 destination.
  • Hex-only registrable names (beef.cafe) were treated as IP literals and skipped the resolved-address check.
  • A resolved address the parsers couldn't decode was treated as public. Now fails closed.
  • metadata.google.internal. (trailing root dot) missed the exact-match blocklist — reachable whenever allowLocalNetwork is set, which is the one check meant to hold unconditionally. Hostnames are canonicalized once at the boundary.
  • Cross-origin redirects replayed credentials and bodies; a 307/308 carrying a body is now refused rather than silently sent bodyless.
  • A ReadableStream body was replayed already-drained across a redirect (hop 1: 19 bytes, hop 2: 0, response 200). FetchHttpClient passes a stream for every streamed body, so this was the common path. Bodies are buffered up front.
  • An abort during the guard rejected with All fibers interrupted without error instead of an AbortError, which callers branching on error.name read as a transport failure and retry.

Known limitation

The guard hands the underlying fetch a hostname, never the address it checked, so the fetch resolves independently and the verdict is advisory: an attacker serving a TTL-0 record wins in the unresolved gap. The cache lengthens that gap from milliseconds to the TTL — it widens an existing hole rather than opening one. Pinning the connection to the checked address is the fix and is not in this PR; a zero TTL would restore the per-request stall without closing anything.

Verification

41 guard tests, 541 sdk tests, lint and typecheck clean. Mutation-tested: 11 mutations, 10 killed. The uncovered one removes response.body?.cancel() on abandoned 3xx responses — Response fixtures have no undici connection to leak, so there's nothing observable to assert.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 3, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing 4b2caab Commit Preview URL

Branch Preview URL
Aug 04 2026, 05:18 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 3, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud 4b2caab Aug 04 2026, 05:20 AM

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Torn down — the PR is closed.

@pkg-pr-new

pkg-pr-new Bot commented Aug 3, 2026

Copy link
Copy Markdown

Open in StackBlitz

@executor-js/cli

npm i https://pkg.pr.new/@executor-js/cli@1524

@executor-js/config

npm i https://pkg.pr.new/@executor-js/config@1524

@executor-js/execution

npm i https://pkg.pr.new/@executor-js/execution@1524

@executor-js/sdk

npm i https://pkg.pr.new/@executor-js/sdk@1524

@executor-js/codemode-core

npm i https://pkg.pr.new/@executor-js/codemode-core@1524

@executor-js/runtime-quickjs

npm i https://pkg.pr.new/@executor-js/runtime-quickjs@1524

@executor-js/plugin-file-secrets

npm i https://pkg.pr.new/@executor-js/plugin-file-secrets@1524

@executor-js/plugin-graphql

npm i https://pkg.pr.new/@executor-js/plugin-graphql@1524

@executor-js/plugin-keychain

npm i https://pkg.pr.new/@executor-js/plugin-keychain@1524

@executor-js/plugin-mcp

npm i https://pkg.pr.new/@executor-js/plugin-mcp@1524

@executor-js/plugin-onepassword

npm i https://pkg.pr.new/@executor-js/plugin-onepassword@1524

@executor-js/plugin-openapi

npm i https://pkg.pr.new/@executor-js/plugin-openapi@1524

executor

npm i https://pkg.pr.new/executor@1524

commit: 78c7062

@RhysSullivan
RhysSullivan force-pushed the fix/hosted-dns-guard-latency branch from 4128736 to c1740be Compare August 3, 2026 20:33
@RhysSullivan
RhysSullivan force-pushed the fix/hosted-dns-guard-latency branch from c1740be to 802114a Compare August 3, 2026 21:00
Refuse a 307/308 that would replay a one-shot body rather than buffering
every upload up front. Also strip all trailing dots when canonicalizing a
hostname, so appending one does not skip the name blocklist.
…oss-origin hops

Every Request input normalizes to a ReadableStream body, which undici
rejects unless the init sets duplex. Rebuild the cross-origin init from an
explicit field list so referrer and credentials cannot ride past the header
safelist.
A dotted quad is legal only at the end of a whole address, and octets are
decimal-only: both forms otherwise decode to a public-looking leading word
while the resolver dials loopback. Also fail an empty resolver answer so the
zero failure TTL drops it instead of blackholing the hostname.
@RhysSullivan
RhysSullivan marked this pull request as ready for review August 4, 2026 17:41
@RhysSullivan
RhysSullivan merged commit 6a924dd into main Aug 4, 2026
20 of 21 checks passed
@RhysSullivan
RhysSullivan deleted the fix/hosted-dns-guard-latency branch August 4, 2026 17:41
RhysSullivan added a commit that referenced this pull request Aug 4, 2026
* Revert "Cache DNS guard resolutions and harden the outbound URL guard (#1524)"

This reverts commit 6a924dd.

* Add changeset for the DNS guard revert
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