Skip to content

fix(security): enforce URL validation across connectors, providers, and auth flows (SSRF + open-redirect hardening)#4236

Open
waleedlatif1 wants to merge 5 commits intostagingfrom
waleedlatif1/workday-ssrf-fix
Open

fix(security): enforce URL validation across connectors, providers, and auth flows (SSRF + open-redirect hardening)#4236
waleedlatif1 wants to merge 5 commits intostagingfrom
waleedlatif1/workday-ssrf-fix

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

@waleedlatif1 waleedlatif1 commented Apr 20, 2026

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-validation so 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 via new 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 to validateExternalUrl, then enforces a ServiceNow-owned hostname allowlist.
  • validateWorkdayTenantUrl — same pattern, enforcing *.workday.com and *.myworkday.com (covers sandbox wd[N]-impl-services[N] and production wd[N]-services[N] + customer-facing *.myworkday.com).
  • 60 new tests (363 total) covering valid hosts, lookalike suffixes, userinfo smuggling, private IPs (169.254.169.254, RFC1918), blocked ports, null/empty, malformed URLs, and case-insensitive matching.

SSRF fixes at the request chokepoints

  • Workday SOAP (tools/workday/soap.ts) — buildWsdlUrl now calls validateWorkdayTenantUrl before 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. Uses validation.sanitized so any future normalization (credential stripping, Unicode) flows through automatically.
  • Azure OpenAI / Azure Anthropic providers — validate user-supplied azureEndpoint via validateUrlWithDNS before instantiating the SDK client. Blocks private IPs, localhost in hosted mode, and dangerous ports; env-provided endpoints bypass the check (trusted).
  • ServiceNow connectorresolveServiceNowInstanceUrl runs validateServiceNowInstanceUrl on every listDocuments, fetchDocument, and validateConfig call before any request is made.
  • Obsidian connector — the vault URL is fully user-controlled (no SaaS domain to allowlist since the Obsidian Local REST API plugin runs on the user's own machine). resolveVaultEndpoint runs the isomorphic validateExternalUrl helper, which blocks private IPs and dangerous ports in hosted mode, forces HTTPS, and carves out 127.0.0.1 / localhost for self-hosted deployments only.
    • Known limitation: the Obsidian connector is reachable from client bundles via 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 pulling dns/promises / http / https into the browser bundle (see commit fa5ab2812). 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

  • Signup form ((auth)/signup/signup-form.tsx) — redirect and callbackUrl query params pass through validateCallbackUrl; invalid values are dropped and logged.
  • Verify flow ((auth)/verify/use-verification.ts) — both sessionStorage.inviteRedirectUrl and ?redirectAfter= are validated; stored unsafe values are actively removed from sessionStorage.

Type of Change

  • Bug fix (security)

Testing

  • Ran full input-validation.test.ts suite — 363 tests passing, including 60 new adversarial cases.
  • Ran bunx turbo run build --filter=sim — compiles successfully (remaining NEXT_PUBLIC_APP_URL collect-page-data error is local-env only).
  • Verified Workday URL patterns against Workday documentation: *.workday.com (sandbox + production service endpoints) and *.myworkday.com (customer-facing production endpoints).
  • Walked through adversarial vectors manually: lookalike suffixes (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

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Apr 20, 2026 3:59pm

Request Review

@cursor
Copy link
Copy Markdown

cursor bot commented Apr 20, 2026

PR Summary

Medium Risk
Touches security-critical redirect and outbound-request paths; while mostly additive validation, it can break previously accepted (but unsafe) URLs and affect connector/provider configuration.

Overview
Hardens user-controlled URL inputs across auth, connectors, and providers. Auth signup/verify flows now validate redirect/callbackUrl/redirectAfter (and stored invite redirects) with a stricter validateCallbackUrl, dropping and logging unsafe values.

Connectors/providers now enforce SSRF guards before making server-side requests: Obsidian vault URLs are resolved/validated via validateExternalUrl, ServiceNow instance URLs must pass a ServiceNow-domain allowlist, Workday SOAP WSDL construction validates tenantUrl, and Azure OpenAI/Anthropic validate user-supplied azureEndpoint via DNS-aware validation. Adds comprehensive tests and new validators (validateServiceNowInstanceUrl, validateWorkdayTenantUrl) to centralize these checks.

Reviewed by Cursor Bugbot for commit 5d26e7b. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 20, 2026

Greptile Summary

This PR hardens SSRF and open-redirect attack surfaces across connectors, providers, and auth flows by centralizing validation logic in input-validation.ts and applying it at every user-controlled URL entry point (Workday SOAP, ServiceNow, Azure OpenAI/Anthropic, signup/verify redirects).

  • P1 – Obsidian DNS rebinding unmitigated: The PR description claims the Obsidian connector resolves hostnames once via validateUrlWithDNS and pins the IP with secureFetchWithPinnedIPAndRetry for all subsequent requests, but neither function appears in the actual code. The connector uses validateExternalUrl + plain fetchWithRetry, and the code comment explicitly confirms "This does not defend against DNS rebinding." The lib/knowledge/documents/utils.ts change (adding the pinning wrapper) referenced in the description is absent from the changed files.

Confidence Score: 4/5

Safe to merge for most surfaces, but the Obsidian DNS-rebinding gap should be resolved or explicitly descoped before merging.

The bulk of the security hardening (ServiceNow allowlist, Workday allowlist, Azure DNS validation, open-redirect guards) is well-implemented and tested. The one P1 finding is the gap between the stated and actual Obsidian protection: the PR advertises DNS-pinning but ships without it, leaving a rebinding vector open.

apps/sim/connectors/obsidian/obsidian.ts — DNS rebinding mitigation described in PR but not implemented

Security Review

  • Obsidian – DNS rebinding (unmitigated): resolveVaultEndpoint validates the vault URL at config time via validateExternalUrl, but all subsequent fetches use plain fetchWithRetry with no IP pinning. An attacker controlling a DNS record could pass validation with a safe IP, then switch the record to a private/metadata IP before the actual fetch, bypassing the SSRF guard. The PR description stated this would be fixed via secureFetchWithPinnedIPAndRetry; that implementation is absent.
  • No credential leakage, injection, or auth-boundary issues found in the remaining changes.

Important Files Changed

Filename Overview
apps/sim/lib/core/security/input-validation.ts Adds validateCallbackUrl (same-origin enforcement via URL API), validateServiceNowInstanceUrl (domain allowlist + HTTPS), and validateWorkdayTenantUrl (domain allowlist + HTTPS); all correctly guard against the described attack vectors.
apps/sim/connectors/obsidian/obsidian.ts Adds resolveVaultEndpoint with validateExternalUrl, but lacks the DNS-pinning protection (secureFetchWithPinnedIPAndRetry) claimed in the PR description; DNS rebinding is unmitigated.
apps/sim/connectors/servicenow/servicenow.ts resolveServiceNowInstanceUrl correctly delegates to validateServiceNowInstanceUrl on every listDocuments/getDocument/validateConfig call, with proper error propagation.
apps/sim/providers/azure-openai/index.ts validateUrlWithDNS applied to user-supplied azureEndpoint only; env-provided endpoint correctly bypasses the check as a trusted value.
apps/sim/providers/azure-anthropic/index.ts Same pattern as Azure OpenAI: validateUrlWithDNS on user-supplied azureEndpoint, env fallback bypasses validation correctly.
apps/sim/tools/workday/soap.ts buildWsdlUrl now validates via validateWorkdayTenantUrl and uses validation.sanitized, protecting all 10 Workday route handlers through a single chokepoint.
apps/sim/app/(auth)/signup/signup-form.tsx validateCallbackUrl applied to redirect/callbackUrl params; logger.warn is called during render rather than in useEffect, which is a minor React rules violation.
apps/sim/app/(auth)/verify/use-verification.ts validateCallbackUrl applied to both sessionStorage-stored redirect and redirectAfter query param; unsafe stored values are actively removed from sessionStorage.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User-supplied URL] --> B{Entry point}
    B --> C[Workday SOAP buildWsdlUrl]
    B --> D[ServiceNow resolveServiceNowInstanceUrl]
    B --> E[Obsidian resolveVaultEndpoint]
    B --> F[Azure OpenAI/Anthropic executeRequest]
    B --> G[Auth signup redirect param]
    B --> H[Auth verify redirectAfter / sessionStorage]
    C --> I[validateWorkdayTenantUrl *.workday.com allowlist]
    D --> J[validateServiceNowInstanceUrl *.service-now.com allowlist]
    E --> K[validateExternalUrl HTTPS + port + IP checks]
    F --> L[validateUrlWithDNS DNS resolution + IP checks]
    G --> M[validateCallbackUrl same-origin check]
    H --> M
    I --> N{Valid?}
    J --> N
    K --> N
    L --> N
    M --> N
    N -->|No| O[Throw / drop / warn]
    N -->|Yes| P[Proceed with request]
    K -.->|No IP pinning| Q[fetchWithRetry DNS rebinding possible]
Loading

Reviews (2): Last reviewed commit: "fix(obsidian): use isomorphic SSRF valid..." | Re-trigger Greptile

Comment thread apps/sim/tools/workday/soap.ts
- 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).
@waleedlatif1 waleedlatif1 changed the title fix(workday): validate tenantUrl to prevent SSRF in SOAP client fix(security): enforce URL validation across connectors, providers, and auth flows (SSRF + open-redirect hardening) Apr 20, 2026
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).
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/connectors/obsidian/obsidian.ts
Comment thread apps/sim/connectors/servicenow/servicenow.ts Outdated
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>
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5d26e7b. Configure here.

if (url && !url.startsWith('https://') && !url.startsWith('http://')) {
url = `https://${url}`
}
const validation = validateExternalUrl(url, 'vaultUrl', { allowHttp: true })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

allowHttp defeats HTTPS enforcement in hosted mode

Medium Severity

The { allowHttp: true } option passed to validateExternalUrl permits HTTP for all hosts in all deployment modes, directly contradicting the JSDoc at line 32 which states "hosted Sim: blocks localhost, private IPs, HTTP (forces HTTPS)." The default behavior of validateExternalUrl without allowHttp already provides the documented policy: HTTPS enforced everywhere except localhost in self-hosted mode (line 728 of input-validation.ts). With allowHttp: true, an admin in hosted mode could configure http://public-host/ as the vault URL, exposing the Bearer access token over plaintext.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5d26e7b. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant