fix(orb): revalidate relay DNS before forwarding#1395
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1395 +/- ##
==========================================
- Coverage 95.36% 95.34% -0.02%
==========================================
Files 197 197
Lines 21464 21480 +16
Branches 7759 7768 +9
==========================================
+ Hits 20470 20481 +11
Misses 416 416
- Partials 578 583 +5
🚀 New features to boost your workflow:
|
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
gittensory-ui | 92452c4 | Commit Preview URL Branch Preview URL |
Jun 25 2026, 11:03 PM |
| .first<{ relay_url: string; relay_secret_enc: string; relay_secret_iv: string; relay_secret_salt: string | null }>(); | ||
| if (!row || !env.TOKEN_ENCRYPTION_SECRET) return "skipped"; | ||
| try { | ||
| if (!(await relayDestinationIsSafe(row.relay_url, resolveHostname))) return "failed"; |
There was a problem hiding this comment.
P1: DNS revalidation uses DoH but fetch still resolves hostname, leaving TOCTOU rebinding risk
DoH IP check does not prevent rebinding because fetchImpl re-resolves the hostname at connection time.
Connect to the validated resolved IP directly, preserving Host header for TLS, instead of passing the hostname to fetch.
AI prompt
Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior.
<file name="src/orb/relay.ts">
<violation number="1" location="src/orb/relay.ts:184">
<priority>P1</priority>
<title>DNS revalidation uses DoH but fetch still resolves hostname, leaving TOCTOU rebinding risk</title>
<evidence>The relayDestinationIsSafe call resolves the hostname via Cloudflare DoH and checks returned IPs, but the subsequent fetchImpl(row.relay_url, ...) still passes the original hostname string, causing the platform's DNS resolver to re-resolve it at connection time. An attacker controlling a relay hostname can serve a safe public IP to Cloudflare's DoH endpoint while returning a private/loopback IP to the Cloudflare Worker runtime resolver, bypassing the safety check.</evidence>
<recommendation>After DNS validation, connect to the resolved IP directly (e.g., reconstruct the request URL using the validated IP and original port/path, and set the Host header to the original hostname for TLS SNI/certificate verification), or use a fetch implementation that enforces the pre-resolved IP.</recommendation>
</violation>
</file>
e8d1523 to
33e2528
Compare
| const answers: string[] = []; | ||
| for (const type of ["A", "AAAA"]) { | ||
| const res = await fetch(`https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(hostname)}&type=${type}`, { headers: { accept: "application/dns-json" }, signal: AbortSignal.timeout(3_000) }); | ||
| if (!res.ok) throw new Error("dns_resolution_failed"); |
There was a problem hiding this comment.
P2: Relay hostname validation leaks internal hostnames to external DNS resolver
Every relay target hostname is sent to Cloudflare's public DoH resolver, leaking internal infrastructure names to a third party.
Use a configurable or local DNS resolver instead of hardcoding cloudflare-dns.com.
AI prompt
Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior.
<file name="src/orb/relay.ts">
<violation number="1" location="src/orb/relay.ts:26">
<priority>P2</priority>
<title>Relay hostname validation leaks internal hostnames to external DNS resolver</title>
<evidence>The resolveRelayHostname function sends every relay target hostname to https://cloudflare-dns.com/dns-query via DNS-over-HTTPS. This exposes potentially private or internal infrastructure hostnames to a third-party service (Cloudflare), creating an information-disclosure risk for enterprise webhook endpoints.</evidence>
<recommendation>Replace the hardcoded public DoH endpoint with a configurable or local DNS resolver so internal hostnames are not leaked to external services, or add an allow-list and documentation noting the Cloudflare query behavior.</recommendation>
</violation>
</file>
Motivation
forwardOrbEventposts stored relay URLs.Description
resolveRelayHostname,relayDestinationIsSafe) insrc/orb/relay.ts.isSafeHttpUrllogic (viaresolvedAddressIsSafe) and reject destinations with no answers or any private/loopback/link-local addresses before attempting the POST.forwardOrbEventfail-safe by returning"failed"when a destination is unsafe or unresolvable instead of throwing, and keep the HMAC-signing/POST flow unchanged for safe destinations.test/integration/orb-relay.test.tsto cover DNS revalidation, rejection of loopback/private-address resolutions, no-DNS-answer behavior, and a positive DNS-checked path; make the resolver pluggable for testing.Testing
npx vitest run test/integration/orb-relay.test.tsand the integration suite passed (21 tests passed).npm run typecheckand TypeScript checked clean with no errors.npx vitest run test/integration/orb-relay.test.ts --coverage.enabled true --coverage.reporter text, but V8 coverage remapping failed after the tests passed withTypeError: jsTokens is not a functioninast-v8-to-istanbul; the functional tests themselves passed despite the coverage tooling error.Codex Task