Skip to content

Commit 6a924dd

Browse files
authored
Cache DNS guard resolutions and harden the outbound URL guard (#1524)
* Cache DNS guard resolutions and harden the outbound URL guard * Add changeset for the DNS guard fix * Pin the redirect mode and the remaining guard arms in tests * Stream request bodies through redirects instead of buffering them 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. * Set duplex for streamed bodies and scrub credential init fields on cross-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. * Reject address forms the platform resolver reads differently 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. * Keep integrity across redirects, drop failed DNS entries, honor undefined init members * Pin range boundaries in outbound guard address tests * Honor caller redirect mode and share one DNS cache across hosted adapters * Note redirect mode and shared cache in changeset * Block the metadata endpoint by address, not hostname string * Note metadata address blocking in the changeset * Check resolved addresses in permissive mode and match fetch redirect semantics
1 parent 59a6640 commit 6a924dd

5 files changed

Lines changed: 2176 additions & 173 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@executor-js/sdk": patch
3+
---
4+
5+
Cache hosted outbound DNS guard resolutions, so a proxied request no longer pays a fresh lookup on every hop. `makeHostedHttp` builds the guarded fetch and the guarded HTTP client layer over one cache; building them separately still works but resolves each hostname twice.
6+
7+
The outbound guard also honors the caller's `redirect` mode, which it previously ignored: `manual` now returns the unfollowed 3xx with its Location header, and `error` rejects, rather than both silently following the redirect. Redirect method semantics now match platform fetch — a `DELETE` or `PUT` meeting a 301/302, and a `HEAD` meeting a 303, keep their method instead of being rewritten to `GET`, so the request the caller made is the request that goes out. Exhausting the redirect budget rejects rather than handing back the raw 3xx as if it were a final response.
8+
9+
Address classification is tightened too: the cloud metadata endpoint is now blocked by the address a hostname denotes rather than by one dotted-decimal spelling, so its IPv6 forms (`::ffff:169.254.169.254`, the 6to4 `2002:a9fe:a9fe::`, NAT64) are blocked under `allowLocalNetwork` as well, and a name that merely resolves to it is blocked in that mode too — the resolved-address check now runs whether or not the local network is allowed, with only the metadata rule applied to its answers when it is; IPv6 prefixes that carry an IPv4 destination (IPv4-translatable, 6to4, local-use NAT64) are classified by that destination; deprecated site-local addresses (`fec0::/10`) count as local; every address a hostname resolves to is checked rather than the first; subresource integrity survives a cross-origin redirect; and address forms the platform resolver reads differently from a decimal-only parser (octal octets, a dotted quad in the head of a compressed literal) no longer classify as public.

‎packages/core/api/src/server/scoped-executor.ts‎

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,7 @@ import {
4343
type ExecutorConfig,
4444
type StorageFailure,
4545
} from "@executor-js/sdk";
46-
import {
47-
makeHostedFetch,
48-
makeHostedHttpClientLayer,
49-
touchSubject,
50-
} from "@executor-js/sdk/host-internal";
46+
import { makeHostedHttp, touchSubject } from "@executor-js/sdk/host-internal";
5147

5248
import { DbProvider } from "./executor-fuma-db";
5349

@@ -269,8 +265,10 @@ export const makeScopedExecutor = <
269265
const hostedHttpOptions = {
270266
allowLocalNetwork: config.allowLocalNetwork,
271267
};
272-
const httpClientLayer = makeHostedHttpClientLayer(hostedHttpOptions);
273-
const hostedFetch = makeHostedFetch(hostedHttpOptions);
268+
// One resolution cache behind both adapters: the plugins take the raw
269+
// fetch and the SDK takes the layer, so building them separately would
270+
// resolve every hostname twice per session.
271+
const { fetch: hostedFetch, httpClientLayer } = makeHostedHttp(hostedHttpOptions);
274272

275273
// The org id is the tenant (catalog partition); the account id is the acting
276274
// subject (drives `owner: "user"` rows). `organizationName` is no longer part
@@ -363,14 +361,15 @@ export const makePlatformExecutor = (
363361
Effect.withSpan("executor.platform.plugins.init"),
364362
);
365363
const hostedHttpOptions = { allowLocalNetwork: config.allowLocalNetwork };
364+
const platformHttp = makeHostedHttp(hostedHttpOptions);
366365

367366
return yield* createExecutor({
368367
tenant: Tenant.make(organizationId),
369368
db,
370369
blobs,
371370
plugins,
372-
httpClientLayer: makeHostedHttpClientLayer(hostedHttpOptions),
373-
fetch: makeHostedFetch(hostedHttpOptions),
371+
httpClientLayer: platformHttp.httpClientLayer,
372+
fetch: platformHttp.fetch,
374373
onElicitation: "accept-all",
375374
platformView: true,
376375
}).pipe(Effect.withSpan("executor.platform.create_executor"));

‎packages/core/sdk/src/host-internal.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
export {
3333
HostedOutboundRequestBlocked,
3434
makeHostedFetch,
35+
makeHostedHttp,
3536
makeHostedHttpClientLayer,
3637
type HostedHttpClientOptions,
3738
} from "./hosted-http-client";

0 commit comments

Comments
 (0)