fix(security): enforce URL validation across connectors, providers, and auth flows (SSRF + open-redirect hardening)#4236
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Auth flows now validate Connectors/providers now validate and sanitize external base URLs before making requests: Obsidian vault endpoints go through Reviewed by Cursor Bugbot for commit b221b6c. Configure here. |
Greptile SummaryThis PR hardens user-controlled URL surfaces against SSRF and open-redirect attacks by centralizing validation through shared helpers in Confidence Score: 5/5Safe to merge — all findings are P2 style concerns that don't affect the security correctness of the changes. 363 tests pass covering adversarial vectors. All SSRF chokepoints (Workday SOAP, Azure providers, ServiceNow, Obsidian) are correctly protected. The only finding is a minor React anti-pattern (side effect in render body) that doesn't impact the security guarantees or runtime correctness. apps/sim/app/(auth)/signup/signup-form.tsx — minor render-body side effect worth addressing. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[User-supplied URL] --> B{Surface type}
B -->|Auth callback| C[validateCallbackUrl]
B -->|Workday tenant| D[validateWorkdayTenantUrl]
B -->|ServiceNow instance| E[validateServiceNowInstanceUrl]
B -->|Obsidian vault| F[validateExternalUrl]
B -->|Azure endpoint| G[validateUrlWithDNS\nserver-only + DNS pinning]
C --> H{origin === app origin?}
D --> I[validateExternalUrl] --> J{*.workday.com\nor *.myworkday.com?}
E --> K[validateExternalUrl] --> L{*.service-now.com\n*.servicenow.com\n*.servicenowservices.com?}
F --> M{HTTPS? No private IP?\nNo blocked port?}
H -->|yes| OK[✅ Accepted]
H -->|no| BLOCK[❌ Blocked]
J -->|yes| OK
J -->|no| BLOCK
L -->|yes| OK
L -->|no| BLOCK
M -->|pass| OK
M -->|fail| BLOCK
G --> N{Private IP?\nLocalhost in hosted?\nDangerous port?}
N -->|clean| OK
N -->|dirty| BLOCK
Reviews (4): Last reviewed commit: "fix(obsidian): drop allowHttp to restore..." | Re-trigger Greptile |
- Azure OpenAI/Anthropic: validate user-supplied azureEndpoint with validateUrlWithDNS to block SSRF to private IPs, localhost (in hosted mode), and dangerous ports. - ServiceNow connector: enforce ServiceNow domain allowlist via validateServiceNowInstanceUrl before calling the instance URL. - Obsidian connector: validate vaultUrl with validateUrlWithDNS and reuse the resolved IP via secureFetchWithPinnedIPAndRetry to block DNS rebinding between validation and request. - Signup + verify flows: pass redirect/callbackUrl/redirectAfter and stored inviteRedirectUrl through validateCallbackUrl; drop unsafe values and log a warning. - lib/knowledge/documents/utils.ts: add secureFetchWithPinnedIPAndRetry wrapper around secureFetchWithPinnedIP (used by Obsidian).
The Obsidian connector is reachable from client bundles via `connectors/registry.ts` (the knowledge UI reads metadata like `.icon`/`.name`). Importing `validateUrlWithDNS` / `secureFetchWithPinnedIP` from `input-validation.server` pulled `dns/promises`, `http`, `https`, `net` into client chunks, breaking the Turbopack build: Module not found: Can't resolve 'dns/promises' ./apps/sim/lib/core/security/input-validation.server.ts [Client Component Browser] ./apps/sim/connectors/obsidian/obsidian.ts [Client Component Browser] ./apps/sim/connectors/registry.ts [Client Component Browser] Once that file polluted a browser context, Turbopack also failed to resolve the Node builtins in its legitimate server-route imports, cascading the error across App Routes and Server Components. Fix: switch the Obsidian connector to the isomorphic `validateExternalUrl` + `fetchWithRetry` helpers, matching the pattern used by every other connector in the registry. This keeps the core SSRF protections: - hosted Sim: blocks localhost, private IPs, HTTP (HTTPS enforced) - self-hosted Sim: allows localhost + HTTP, still blocks non-loopback private IPs and dangerous ports (22, 25, 3306, 5432, 6379, 27017, 9200) Drops the DNS-rebinding defense specifically (the IP-pinned fetch chain). The trade-off is acceptable because the vault URL is entered by the workspace admin — not arbitrary untrusted input — and hosted deployments already force the plugin to be exposed through a public URL (tunnel/port-forward), making rebinding a narrow threat. Also reverts the `secureFetchWithPinnedIPAndRetry` wrapper in `lib/knowledge/documents/utils.ts` (no longer needed, and its `.server` import was the original source of the client-bundle pollution).
|
@greptile |
|
@cursor review |
Match listDocuments behavior — invalid instance URL should surface as a configuration error rather than being swallowed into a "document not found" null response during sync. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
@greptile |
|
@cursor review |
…mode allowHttp: true permitted plaintext HTTP for all hosts in all deployment modes, contradicting the documented policy. The default validateExternalUrl behavior already allows http://localhost in self-hosted mode (the actual Obsidian Local REST API use case) via the built-in carve-out, while correctly rejecting HTTP for public hosts in hosted mode — which prevents leaking the Bearer access token over plaintext. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
@greptile |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit b221b6c. Configure here.
Summary
Hardens user-controlled URL surfaces across the app against SSRF (server-side forgery to internal metadata endpoints, private IPs, arbitrary hosts) and open-redirect attacks on auth flows. Every fix is centralized through shared validators in
@/lib/core/security/input-validationso each consumer is a thin call site.New / updated validators (
lib/core/security/input-validation.ts)validateCallbackUrl— rewritten to resolve against a safe server-side base vianew URL(), rejecting protocol-relative (//evil.com), backslash/whitespace bypasses, userinfo smuggling (https://safe@evil.com), and non-HTTP schemes while still accepting legitimate same-origin relative/absolute callbacks.validateServiceNowInstanceUrl— delegates HTTPS + private-IP + port checks tovalidateExternalUrl, then enforces a ServiceNow-owned hostname allowlist.validateWorkdayTenantUrl— same pattern, enforcing*.workday.comand*.myworkday.com(covers sandboxwd[N]-impl-services[N]and productionwd[N]-services[N]+ customer-facing*.myworkday.com).169.254.169.254, RFC1918), blocked ports, null/empty, malformed URLs, and case-insensitive matching.SSRF fixes at the request chokepoints
tools/workday/soap.ts) —buildWsdlUrlnow callsvalidateWorkdayTenantUrlbefore constructing the WSDL URL. This is the single code path for all 10 Workday route handlers, so every operation (get_workers,get_job_postings,submit_time_off, …) is protected without per-route duplication. Usesvalidation.sanitizedso any future normalization (credential stripping, Unicode) flows through automatically.azureEndpointviavalidateUrlWithDNSbefore instantiating the SDK client. Blocks private IPs, localhost in hosted mode, and dangerous ports; env-provided endpoints bypass the check (trusted).resolveServiceNowInstanceUrlrunsvalidateServiceNowInstanceUrlon everylistDocuments,fetchDocument, andvalidateConfigcall before any request is made.resolveVaultEndpointruns the isomorphicvalidateExternalUrlhelper, which blocks private IPs and dangerous ports in hosted mode, forces HTTPS, and carves out127.0.0.1/localhostfor self-hosted deployments only.connectors/registry.ts(the knowledge UI reads.icon/.name), which forces the connector module graph to stay client-safe. This means the DNS-pinned fetch chain (which would also defend against DNS rebinding) cannot live in the connector without pullingdns/promises/http/httpsinto the browser bundle (see commitfa5ab2812). The remaining attack requires an admin-entered hostname that flips between validation and request — narrow because the vault URL is entered by the workspace admin (trusted), and hosted deployments already require exposing the plugin through a public tunnel. Closing this gap will require a registry architecture split (metadata registry for client, handler registry for server), tracked as a follow-up.Open-redirect fixes on auth flows
(auth)/signup/signup-form.tsx) —redirectandcallbackUrlquery params pass throughvalidateCallbackUrl; invalid values are dropped and logged.(auth)/verify/use-verification.ts) — bothsessionStorage.inviteRedirectUrland?redirectAfter=are validated; stored unsafe values are actively removed from sessionStorage.Type of Change
Testing
input-validation.test.tssuite — 363 tests passing, including 60 new adversarial cases.bunx turbo run build --filter=sim— compiles successfully (remainingNEXT_PUBLIC_APP_URLcollect-page-data error is local-env only).*.workday.com(sandbox + production service endpoints) and*.myworkday.com(customer-facing production endpoints).evilworkday.com), embedded substrings (workday.com.evil.com), userinfo smuggling (https://attacker.com@wd5.workday.com), protocol-relative (//evil.com), backslash tricks — all correctly blocked.Checklist