Skip to content

fix(orb): revalidate relay DNS before forwarding#1395

Closed
JSONbored wants to merge 4 commits into
mainfrom
codex/fix-dns-based-blind-ssrf-vulnerability
Closed

fix(orb): revalidate relay DNS before forwarding#1395
JSONbored wants to merge 4 commits into
mainfrom
codex/fix-dns-based-blind-ssrf-vulnerability

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Motivation

  • The relay registration guard only checked URL syntax and the textual hostname, which permits DNS-based rebinding to private/loopback addresses at fetch time and creates a blind SSRF primitive when forwardOrbEvent posts stored relay URLs.
  • Forwarding must therefore revalidate the resolved addresses at send time to prevent attacker-controlled hostnames from resolving to internal addresses.
  • The change preserves the existing best-effort/fail-safe behavior so a forwarding failure never breaks the Orb webhook 202 path.

Description

  • Add forward-time DNS resolution for relay targets using a pluggable resolver and DNS-over-HTTPS lookups to obtain A/AAAA answers (resolveRelayHostname, relayDestinationIsSafe) in src/orb/relay.ts.
  • Validate each resolved address with the existing isSafeHttpUrl logic (via resolvedAddressIsSafe) and reject destinations with no answers or any private/loopback/link-local addresses before attempting the POST.
  • Keep forwardOrbEvent fail-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.
  • Add regression tests in test/integration/orb-relay.test.ts to 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

  • Ran npx vitest run test/integration/orb-relay.test.ts and the integration suite passed (21 tests passed).
  • Ran npm run typecheck and TypeScript checked clean with no errors.
  • Attempted coverage with npx vitest run test/integration/orb-relay.test.ts --coverage.enabled true --coverage.reporter text, but V8 coverage remapping failed after the tests passed with TypeError: jsTokens is not a function in ast-v8-to-istanbul; the functional tests themselves passed despite the coverage tooling error.

Codex Task

@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jun 25, 2026
@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.75000% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.36%. Comparing base (9e1c351) to head (394aad9).
⚠️ Report is 4 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/orb/relay.ts 68.75% 0 Missing and 5 partials ⚠️

❌ Your patch check has failed because the patch coverage (68.75%) is below the target coverage (97.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1395      +/-   ##
==========================================
- Coverage   95.38%   95.36%   -0.02%     
==========================================
  Files         201      201              
  Lines       21598    21614      +16     
  Branches     7807     7816       +9     
==========================================
+ Hits        20601    20612      +11     
  Misses        416      416              
- Partials      581      586       +5     
Files with missing lines Coverage Δ
src/orb/relay.ts 92.30% <68.75%> (-7.70%) ⬇️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 25, 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
gittensory-ui 92452c4 Commit Preview URL

Branch Preview URL
Jun 25 2026, 11:03 PM

Comment thread src/orb/relay.ts
.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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@superagent-security superagent-security Bot added the pr:flagged PR flagged for review by security analysis. label Jun 25, 2026
@JSONbored JSONbored self-assigned this Jun 26, 2026
@JSONbored JSONbored added the gittensor:bug Gittensor-scored bug fix - worth 0.5x multiplier. label Jun 26, 2026
@JSONbored JSONbored force-pushed the codex/fix-dns-based-blind-ssrf-vulnerability branch from e8d1523 to 33e2528 Compare June 26, 2026 20:53
Comment thread src/orb/relay.ts
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");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@gittensory-orb

gittensory-orb Bot commented Jun 27, 2026

Copy link
Copy Markdown

Caution

🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥

🛑 Gittensory review — blocked

2 files · 1 AI reviewers · no blockers · readiness 48/100 · CI failing · blocked

🛑 Blocked

CI checks failing

  • codecov/patch — 68.75% of diff hit (target 97.00%)
Signal Result Evidence
Code review ✅ No blockers 1 reviewers, synthesized
Linked issue ⚠️ Missing No linked issue or no-issue rationale found.
Related work ⚠️ 3 scoped overlaps Top overlaps are listed below; lower-confidence bulk is hidden.
Review load ❌ 8/20 Readiness component derived from cached public PR metadata and labels; size label size:M.
Validation evidence ❌ 5/25 Cached preflight status is hold.
Open PR queue ❌ 3/10 48 open PR(s), 9 likely reviewable, 39 unlinked.
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 81 PR(s), 261 issue(s).
Gate result ✅ Passing No configured blocker found.
Nits — 2 non-blocking
  • Repository config was not parsed
  • No linked issue detected — If this PR is intended to solve an issue, link it explicitly in the PR body.
Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository registration is not available in the local Gittensory cache.
  • Public profile languages: not available
  • Official Gittensor activity: 81 PR(s), 261 issue(s).
  • Related work: Titles/paths share 8 meaningful terms. (PR #1397)
  • Related work: Titles/paths share 8 meaningful terms. (PR #1427, PR #1489)
  • Related work: Titles/paths share 7 meaningful terms. (PR #1537, PR #1546)
  • Additional title-only matches omitted; title-only overlap does not block.
Contributor next steps
  • Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Explain no-issue PR.
  • Review top overlaps.
  • Add scope summary.
  • Fix blocker.
  • Expect slower review.
  • Refresh registry data or choose a registered active repo.
  • Link the issue being solved, or explicitly explain why this is a no-issue PR.
  • Check active issues and PRs before submitting.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Review load = cached public PR metadata such as size labels, changed paths, and preflight status.
  • Open PR queue = repo-wide review pressure; it is not a PR quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.

  • Re-run Gittensory review

@gittensory-orb gittensory-orb Bot added the gittensor Gittensor contributor context label Jun 27, 2026
@JSONbored

Copy link
Copy Markdown
Owner Author

Closing — codecov/patch is red, and the fix is only partial. forwardOrbEvent still POSTs to row.relay_url (the hostname, relay.ts:167), not the address it just validated, so a fast DNS rebind between the DoH check and the Workers fetch() re-resolution leaves the TOCTOU open — while adding a DNS round-trip to every forwarded event. The gap is real but low-severity (a relay can only target its own enrollment's forward path). If we want to close it properly, the fix is resolve-and-pin (or a private-target-blocking fetch), not re-resolve-then-POST-the-hostname.

@JSONbored JSONbored closed this Jun 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

aardvark codex gittensor:bug Gittensor-scored bug fix - worth 0.5x multiplier. gittensor Gittensor contributor context pr:flagged PR flagged for review by security analysis. size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant