feat: relayauth wave 3 — landing page + discovery ecosystem (WF100-110)#4
feat: relayauth wave 3 — landing page + discovery ecosystem (WF100-110)#4khaliqgant merged 21 commits intomainfrom
Conversation
… and test assertions - Add universal wildcard `*` support to scope parser (prevents InvalidScopeError for tokens with scopes: ["*"]) - Add missing AuditAction variants: budget.exceeded, budget.alert, scope.escalation_denied - Fix SDK build script to use `tsc --build --force` for composite project emit - Fix e2e test to use HMAC auth instead of JWKS-based TokenVerifier - Align test assertions with implementation (CSV injection escaping, webhook secret masking, mock data ordering) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Addressed the Devin findings in the latest push ().\n\nFixed:\n- RelayAuthAdapter now invalidates the cached client after token rotation so subsequent calls use the newly issued token\n- discovery caching now clears a rejected promise so transient failures can retry\n- discovery bridge now uses manual redirect handling with per-hop private-host validation to block SSRF via redirects\n\nAlso added regression coverage for the adapter token/discovery cases and the private-redirect bridge case.\n\nRe-verified locally:\n- ✅\n- ✅ |
|
Correction to previous comment: addressed the Devin findings in latest push (ac41c7d). Fixed:
Added regression coverage for the adapter token/discovery cases and the private-redirect bridge case. Re-verified locally:
|
Security (HIGH/MEDIUM): - discovery.ts: SSRF TOCTOU fix — manual redirect following with per-hop validation - discovery.ts: Private host blocklist expanded with CGNAT 100.64.0.0/10, DNS-rebinding services - discovery.ts: Bridge endpoint now requires Bearer token auth - discovery.ts: Origin spoofing mitigated via BASE_URL env var - adapter.ts: executeWithAuth hardened — SSRF checks, redirect blocking, timeouts - adapter.ts: fetchConfiguration — 5s timeout, 1MB response limit - adapter.ts: Runtime type validation for all executeTool parameters - adapter.ts: Stale token cache invalidation via #clientToken tracking Code quality (MEDIUM/LOW): - openai.ts: Removed double adapter instantiation, uses RELAYAUTH_TOOLS directly - anthropic.ts/openai.ts/vercel.ts: Extracted shared errorResult to utils.ts - vercel.ts: Removed duplicate SchemaLike type, uses zod schemas - openapi-scopes.ts: Renamed to OpenAPIScopeDefinition to avoid collision - scope.ts: Replaced type cast with new Uint8Array() - package.json: Widened zod peer dep to ^3.23.0 - adapter.ts: Replaced JSON.stringify comparison with arraysEqual - worker.ts: Added TODO for CORS restriction - init-wizard.ts: Added doc comment noting YAML parser limitations - discovery.ts: Added TODO for deep import (SDK dist rebuild needed) Deferred: deep SDK import (dist not yet rebuilt), hardcoded versions (CF Worker constraint) Not actionable: YAML parser (adequate for use case), unused import (false positive), revert history (observational) Co-Authored-By: My Senior Dev <dev@myseniordev.com>
adapter.ts: client lifecycle — stale token after issuance (resolved in prior commits) adapter.ts: discovery promise — failed fetch permanently cached (resolved in prior commits) discovery.ts: SSRF redirect TOCTOU — redirect: manual with per-hop validation (resolved in prior commits) verify.ts: token expiration leeway — restore 30s clock-skew tolerance matching nbf discovery.ts: SSRF bypass via IPv6-mapped IPv4 — handle both dotted and hex notation Co-Authored-By: My Senior Dev <dev@myseniordev.com>
discovery.ts: secure bridge endpoint with requireScope middleware instead of unverified token extraction discovery.ts: change break to continue in fetchAgentCard for proper 404 fallback Co-Authored-By: My Senior Dev <dev@myseniordev.com>
efd7475 to
a128269
Compare
| function isPrivateUrl(url: URL): boolean { | ||
| if (url.protocol !== "https:" && url.protocol !== "http:") return true; | ||
| const h = url.hostname; | ||
| if (h === "localhost" || h === "[::1]") return true; | ||
| // IPv4 private/reserved ranges | ||
| const ipv4 = h.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/); | ||
| if (ipv4) { | ||
| const [, a, b] = ipv4.map(Number); | ||
| if (a === 10) return true; // 10.0.0.0/8 | ||
| if (a === 172 && b >= 16 && b <= 31) return true; // 172.16.0.0/12 | ||
| if (a === 192 && b === 168) return true; // 192.168.0.0/16 | ||
| if (a === 127) return true; // 127.0.0.0/8 | ||
| if (a === 169 && b === 254) return true; // link-local | ||
| if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT 100.64.0.0/10 | ||
| if (a === 0) return true; // 0.0.0.0/8 | ||
| } | ||
| return false; | ||
| } |
There was a problem hiding this comment.
🔴 AI adapter SSRF bypass via IPv6 hex-mapped IPv4 addresses (e.g., ::ffff:7f00:1)
The isPrivateUrl function in the AI adapter fails to block IPv6-mapped IPv4 addresses in hex notation. Node.js's URL API normalizes http://[::ffff:127.0.0.1] so that hostname becomes ::ffff:7f00:1. The regex on line 79-81 (/^::(?:ffff:)?(\d{1,3})\.(\d{1,3})\.\d{1,3}\.\d{1,3}$/) only matches the dotted form, so the hex form falls through to return false at line 86, allowing SSRF to loopback. The server-side isPrivateHost in packages/server/src/routes/discovery.ts:657-668 correctly handles this case with explicit hex-colon parsing. An attacker who controls LLM tool parameters could use executeWithAuth to reach internal services via URLs like http://[::ffff:7f00:1]/.
Was this helpful? React with 👍 or 👎 to provide feedback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
worker.ts: CORS origin restriction via ALLOWED_ORIGINS env var worker.ts: default-deny auth middleware with PUBLIC_PATHS whitelist worker.ts: per-IP rate limiting on /v1/discovery/bridge endpoint identity-do.ts: x-internal-secret header verification on /internal/* endpoints adapter.ts: complete IPv6 SSRF coverage in isPrivateUrl() discovery.ts: cloud metadata hostname blocking and shared SSRF utility export discovery.ts: hostname/host confusion SSRF bypass prevention Co-Authored-By: My Senior Dev <dev@myseniordev.com>
|
|
||
| // Block known DNS-rebinding services | ||
| if ( | ||
| h.endsWith(".nip.io") || | ||
| h.endsWith(".sslip.io") || | ||
| h.endsWith(".xip.io") | ||
| ) { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
🔴 AI adapter SSRF bypass via cloud metadata hostnames (metadata.google.internal)
The isPrivateUrl function in the AI adapter does not block cloud metadata service hostnames (metadata.google.internal, metadata.goog). While 169.254.169.254 is caught by the isPrivateIPv4(169, 254) check, hostname-based cloud metadata endpoints bypass all checks. The server-side isPrivateHost in packages/server/src/routes/discovery.ts:611-618 blocks these explicitly. Since executeWithAuth sends the user's bearer token to arbitrary URLs, an attacker controlling LLM tool parameters could exfiltrate cloud credentials (e.g., GCP service account tokens via http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token).
| // Block known DNS-rebinding services | |
| if ( | |
| h.endsWith(".nip.io") || | |
| h.endsWith(".sslip.io") || | |
| h.endsWith(".xip.io") | |
| ) { | |
| return true; | |
| } | |
| // Block known DNS-rebinding services | |
| if ( | |
| h.endsWith(".nip.io") || | |
| h.endsWith(".sslip.io") || | |
| h.endsWith(".xip.io") | |
| ) { | |
| return true; | |
| } | |
| // Block cloud metadata endpoints (AWS, GCP, Azure) | |
| if ( | |
| h === "metadata.google.internal" || | |
| h.endsWith(".metadata.google.internal") || | |
| h === "metadata.goog" | |
| ) { | |
| return true; | |
| } |
Was this helpful? React with 👍 or 👎 to provide feedback.
| const bridgeRateMap = new Map<string, { count: number; resetAt: number }>(); | ||
|
|
||
| app.use("/v1/discovery/bridge", async (c, next) => { | ||
| const ip = c.req.header("cf-connecting-ip") ?? c.req.header("x-forwarded-for") ?? "unknown"; | ||
| const now = Date.now(); | ||
|
|
||
| let entry = bridgeRateMap.get(ip); | ||
| if (!entry || now >= entry.resetAt) { | ||
| entry = { count: 0, resetAt: now + BRIDGE_RATE_WINDOW_MS }; | ||
| bridgeRateMap.set(ip, entry); | ||
| } | ||
|
|
||
| entry.count++; | ||
| if (entry.count > BRIDGE_RATE_LIMIT) { | ||
| return c.json({ error: "Rate limit exceeded", code: "rate_limited" }, 429); | ||
| } | ||
|
|
||
| await next(); | ||
| }); |
There was a problem hiding this comment.
🟡 Unbounded memory growth in bridgeRateMap due to never-evicted entries
The bridgeRateMap module-level Map in worker.ts stores per-IP rate limit counters but never evicts stale entries. When an IP's window expires, its entry remains in the map until the same IP makes another request. In a high-traffic environment, every unique IP that ever hits /v1/discovery/bridge permanently consumes memory in the map. While Cloudflare Worker isolate recycling somewhat mitigates this, within a single isolate's lifetime the map grows without bound proportional to unique IPs.
Prompt for agents
In packages/server/src/worker.ts, the bridgeRateMap (line 77) is an unbounded Map that never evicts expired entries. Add periodic cleanup: after checking/updating the current IP's entry, iterate the map and delete any entries where now >= entry.resetAt. Alternatively, cap the map size (e.g., if bridgeRateMap.size > 10000, clear the entire map). This prevents unbounded memory growth in long-lived isolates.
Was this helpful? React with 👍 or 👎 to provide feedback.
Relayauth follow-on wave after PR #2
PR #2 contained the major core build and has already been merged to
main.This PR is not the whole relayauth effort. It is the incremental follow-on wave that landed after PR #2, covering the landing page plus the new Discovery & Ecosystem domain.
What already happened in PR #2 (merged)
PR #2 delivered the main relayauth platform build, including the core foundation:
That work is already merged and is not duplicated here.
What this PR adds
This PR contains the next incremental wave on top of merged
main:WF100 — Landing page
packages/landingDomain 13 — Discovery & Ecosystem (WF101–110)
Important context
astro checkwas prompting interactively for@astrojs/check; that was fixed by adding the required packages and addressing a small Astro type issue.main; this PR captures that new wave.Review focus
/.well-knowndiscovery contract