Skip to content

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

Merged
waleedlatif1 merged 6 commits intostagingfrom
waleedlatif1/workday-ssrf-fix
Apr 20, 2026
Merged

fix(security): enforce URL validation across connectors, providers, and auth flows (SSRF + open-redirect hardening)#4236
waleedlatif1 merged 6 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 4:31pm

Request Review

@cursor
Copy link
Copy Markdown

cursor bot commented Apr 20, 2026

PR Summary

Medium Risk
Tightens validation on user-controlled redirect and endpoint URLs across auth flows and multiple integrations, which can block previously-working (but unsafe) configurations and affect sign-up/verification and connector/provider connectivity.

Overview
Hardens user-controlled URLs across auth, connectors, and LLM providers to reduce open-redirect and SSRF risk.

Auth flows now validate redirect/callbackUrl/redirectAfter via a rewritten validateCallbackUrl (URL-parsing based), drop unsafe values, and log/clear unsafe sessionStorage redirect data.

Connectors/providers now validate and sanitize external base URLs before making requests: Obsidian vault endpoints go through validateExternalUrl, ServiceNow instance URLs are allowlisted to ServiceNow-owned domains, Workday SOAP WSDL construction enforces Workday-hosted tenant URLs, and Azure OpenAI/Anthropic block unsafe user-supplied azureEndpoint values via DNS-backed validation. Expanded tests cover these new validators and edge-case bypass payloads.

Reviewed by Cursor Bugbot for commit b221b6c. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 20, 2026

Greptile Summary

This PR hardens user-controlled URL surfaces against SSRF and open-redirect attacks by centralizing validation through shared helpers in input-validation.ts. New domain-allowlist validators cover Workday, ServiceNow, and callback URLs; validateUrlWithDNS (DNS-pinned) gates Azure provider endpoints; and the Obsidian connector uses the isomorphic validateExternalUrl to stay client-bundle safe.

Confidence Score: 5/5

Safe 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

Filename Overview
apps/sim/lib/core/security/input-validation.ts Adds validateCallbackUrl (open-redirect hardening), validateServiceNowInstanceUrl, and validateWorkdayTenantUrl (SSRF allowlisting); all correctly delegate to validateExternalUrl for IP/port/protocol checks then enforce domain suffixes.
apps/sim/app/(auth)/signup/signup-form.tsx Adds validateCallbackUrl check for the redirect/callbackUrl query params; minor anti-pattern: logging side effect runs in render body rather than useEffect.
apps/sim/app/(auth)/verify/use-verification.ts Validates both sessionStorage invite redirect URL and redirectAfter query param inside useEffect — correct placement for side effects.
apps/sim/connectors/obsidian/obsidian.ts resolveVaultEndpoint now calls validateExternalUrl; uses the isomorphic validator (no DNS-pinned fetch) to stay client-bundle-safe; acknowledged DNS rebinding limitation is reasonable given the architecture constraint.
apps/sim/connectors/servicenow/servicenow.ts resolveServiceNowInstanceUrl applies allowlist validation before any fetch; uses validation.sanitized for the return value.
apps/sim/providers/azure-openai/index.ts User-provided azureEndpoint is gated through validateUrlWithDNS (server-only, DNS-pinned); env-provided endpoint bypasses the check as intended.
apps/sim/providers/azure-anthropic/index.ts Same pattern as Azure OpenAI — user-provided endpoint validated, env-provided trusted; implementation is clean and symmetric.
apps/sim/tools/workday/soap.ts buildWsdlUrl now validates tenantUrl through validateWorkdayTenantUrl before URL construction; uses validation.sanitized to capture any future normalization.
apps/sim/lib/core/security/input-validation.test.ts 363 tests (60 new) covering adversarial vectors — lookalike suffixes, userinfo smuggling, private IPs, protocol-relative, backslash tricks, and port blocking.

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
Loading

Reviews (4): Last reviewed commit: "fix(obsidian): drop allowHttp to restore..." | 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

Comment thread apps/sim/connectors/obsidian/obsidian.ts Outdated
…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>
@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.

✅ 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.

@waleedlatif1 waleedlatif1 merged commit febc36f into staging Apr 20, 2026
14 checks passed
@waleedlatif1 waleedlatif1 deleted the waleedlatif1/workday-ssrf-fix branch April 20, 2026 17:54
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