Skip to content

fix(security): harden SSO domain registration, webhook path isolation, and CSV export#4813

Merged
waleedlatif1 merged 17 commits into
stagingfrom
waleedlatif1/verify-code-changes
May 31, 2026
Merged

fix(security): harden SSO domain registration, webhook path isolation, and CSV export#4813
waleedlatif1 merged 17 commits into
stagingfrom
waleedlatif1/verify-code-changes

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

@waleedlatif1 waleedlatif1 commented May 30, 2026

Summary

  • Normalize SSO registration domains and reject cross-tenant domain claims (409)
  • Enforce single-workflow ownership on webhook path delivery and block cross-workflow path claims at deploy time (409)
  • Neutralize CSV formula injection in table exports (cell values and header names)

Type of Change

  • Bug fix

Testing

Tested manually; unit tests added for each fix (run in CI — this workspace has no local node_modules)

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)

…k path isolation, env secrets, and CSV export
@vercel
Copy link
Copy Markdown

vercel Bot commented May 30, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped May 31, 2026 12:18am

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 30, 2026

PR Summary

High Risk
Changes authentication domain ownership, webhook routing across tenants, and export behavior—security-sensitive paths where regressions could block legitimate SSO updates or mis-route webhooks.

Overview
Hardens SSO registration, webhook paths, and table CSV export, with matching unit tests. Docs/app integration icons get SVG viewBox/path tweaks only.

SSO: Domains are normalized via normalizeSSODomain before save. Registration checks existing providers case-insensitively and returns 409 (SSO_DOMAIN_ALREADY_REGISTERED) when another tenant owns the domain, while still allowing the same org (or the caller’s user-scoped provider) to update.

Webhooks: findConflictingWebhookPathOwner blocks claiming a path already used by another workflow at create/upsert and deploy (409 / deploy error). At delivery time, findAllWebhooksForPath keeps only the earliest workflow’s webhooks when the same path collides across tenants, preserving credential-set fan-out within that owner.

CSV export: String headers and cells that start with formula triggers (=, +, -, @, etc.) are prefixed to reduce spreadsheet injection risk.

Reviewed by Cursor Bugbot for commit 9119b6a. Configure here.

Comment thread apps/sim/app/api/files/authorization.ts Outdated
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 30, 2026

Greptile Summary

This PR hardens three distinct attack surfaces: SSO domain registration normalizes and canonicalizes the domain before conflict-checking (preventing cross-tenant hijacking via casing/format variants), webhook path deployment and delivery enforce single-workflow ownership (blocking a second tenant from registering a path already owned by another, with a runtime fallback for pre-existing collisions), and the CSV table export neutralizes formula-injection triggers in both headers and cell values.

  • SSO: normalizeSSODomain strips protocol, path, email prefix, wildcards, and trailing dots before a case-insensitive DB lookup; an isOwnedByCaller predicate allows same-user and same-org re-registration while blocking all other tenants.
  • Webhooks: findConflictingWebhookPathOwner is the single gate at both the UI-facing save endpoint and the deploy pipeline; findAllWebhooksForPath adds runtime defense-in-depth by electing the earliest-createdAt workflow as path owner when a pre-existing collision slips through.
  • CSV export: neutralizeCsvFormula prefixes ' to any string value beginning with =, +, -, @, tab, or CR before serialization, following the standard spreadsheet text-prefix approach.

Confidence Score: 5/5

Safe to merge - all three security fixes are logically sound and backed by focused unit tests.

The domain-normalization logic, ownership predicate, and webhook path isolation checks are all correct and well-tested. The runtime fallback in findAllWebhooksForPath provides defense-in-depth even for pre-existing collisions. The CSV formula neutralization follows the standard single-quote text-prefix approach and interacts correctly with escapeCsvField. No broken contract, no data-loss path, and no new auth boundary issue was found.

apps/sim/lib/webhooks/utils.server.ts - the tx parameter on findConflictingWebhookPathOwner is never used at either call site, leaving a narrow concurrent-deploy race.

Important Files Changed

Filename Overview
apps/sim/lib/auth/sso/domain.ts New helper that normalizes SSO domains: strips protocol, path, wildcard, email local part, trailing dot, then validates with a registrable-domain regex. Correctly handles casing variants.
apps/sim/app/api/auth/sso/register/route.ts Adds cross-tenant domain claim protection: normalizes the domain via normalizeSSODomain, queries only candidate rows with a SQL LOWER(domain) filter, and rejects with 409 if any non-owned existing provider matches.
apps/sim/lib/webhooks/utils.server.ts Adds findConflictingWebhookPathOwner - the single guard against cross-workflow path collisions at registration/deploy time. Exposes a tx parameter for transactional use but neither call site currently passes one, leaving a narrow TOCTOU window.
apps/sim/app/api/webhooks/route.ts Replaces the old single-query path check with a two-step approach: first calls findConflictingWebhookPathOwner (409 on cross-workflow conflict), then a separate query to find an existing own-workflow webhook to reuse.
apps/sim/lib/webhooks/deploy.ts Adds a pre-deploy findConflictingWebhookPathOwner check inside saveTriggerWebhooksForDeploy, returning a 409-equivalent error object when a different workflow owns the requested path.
apps/sim/lib/webhooks/processor.ts Adds runtime defense-in-depth: when findAllWebhooksForPath encounters webhooks for multiple workflows on the same path, it selects the earliest-createdAt workflow as owner and drops foreign rows, preventing cross-tenant delivery of pre-existing collisions.
apps/sim/app/api/table/[tableId]/export/route.ts Adds neutralizeCsvFormula that prefixes a single quote to any cell/header value starting with a spreadsheet formula trigger. Applied to both headers and string cell values before toCsvRow/escapeCsvField.
apps/sim/lib/auth/sso/domain.test.ts Unit tests for normalizeSSODomain covering casing, protocol stripping, wildcard/email-prefix forms, trailing dot, and rejection of non-registrable inputs.
apps/sim/app/api/auth/sso/register/route.test.ts Integration-style tests for the SSO register route, covering billing gate, admin check, domain validation, cross-tenant block, casing variants, and the domain normalisation-before-persistence check.
apps/sim/lib/webhooks/processor.test.ts New test suite for findAllWebhooksForPath cross-tenant isolation logic: covers single-workflow fan-out, collision resolution to earliest owner, credential-set preservation, empty-path, and empty-result cases.
apps/docs/components/icons.tsx Cosmetic icon updates: viewBox corrections for Serper, S3, Telegram; replaces BrainIcon and EvernoteIcon SVG paths; adds higher-fidelity MicrosoftOneDriveIcon with gradient definitions using useId; updates GreptileIcon.

Sequence Diagram

sequenceDiagram
    participant Client
    participant WebhookRoute as POST /api/webhooks
    participant DeployFn as saveTriggerWebhooksForDeploy
    participant Guard as findConflictingWebhookPathOwner
    participant DB
    participant Processor as findAllWebhooksForPath (runtime)

    Note over Client,DB: Registration path (route.ts)
    Client->>WebhookRoute: "POST {path, workflowId}"
    WebhookRoute->>Guard: "{path, workflowId}"
    Guard->>DB: "SELECT workflowId WHERE path=X AND isActive=true"
    DB-->>Guard: rows
    Guard-->>WebhookRoute: "conflictingWorkflowId | null"
    alt conflict from another workflow
        WebhookRoute-->>Client: 409 PATH_EXISTS
    else no conflict
        WebhookRoute->>DB: upsert webhook
        WebhookRoute-->>Client: 200 OK
    end

    Note over Client,DB: Deploy path (deploy.ts)
    Client->>DeployFn: deploy workflow
    DeployFn->>Guard: "{path, workflowId}"
    Guard-->>DeployFn: "conflictingWorkflowId | null"
    alt conflict detected
        DeployFn-->>Client: error 409
    else clear
        DeployFn->>DB: save webhook config
    end

    Note over Client,DB: Runtime delivery (processor.ts)
    Client->>Processor: incoming request for path
    Processor->>DB: SELECT all active webhooks for path
    DB-->>Processor: rows
    alt cross-workflow collision
        Processor->>Processor: pick earliest createdAt as owner
        Processor-->>Client: dispatch only to owner workflow
    else single workflow
        Processor-->>Client: dispatch normally
    end
Loading

Reviews (6): Last reviewed commit: "refactor(security): consolidate webhook ..." | Re-trigger Greptile

Comment thread apps/sim/app/api/files/authorization.ts Outdated
Comment thread apps/sim/app/api/auth/sso/register/route.ts Outdated
Address PR review: avoid a full-table scan on every SSO provider
registration by filtering candidate rows in SQL with
lower(domain) = <normalized>, keeping the in-memory ownership check.
Also tighten the normalizeSSODomain TSDoc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
waleedlatif1 and others added 3 commits May 30, 2026 12:06
…thorization

Condense verbose comment blocks to concise TSDoc/single-line form; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the bypassable isInternalFileUrl substring check in resolveInternalKbKey
with an origin allow-list (base URL, internal API base URL, TRUSTED_ORIGINS).
A crafted external host whose path is /api/files/serve/<victim-key> no longer
resolves to the victim key. Relative same-origin URLs are unaffected.
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

…query

Match the repo's prevailing `sql`lower(col) = value`` idiom for the
case-insensitive SSO domain conflict lookup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ccess

Use the same admin check the secrets UI uses (owner, admin permission, or
org-admin) so owners and org-admins are not wrongly denied their own decrypted
workspace secrets, while read-only members remain restricted to names only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread apps/sim/app/api/auth/sso/register/route.ts Outdated
…ad in-memory recheck

Address PR review: the SQL `lower(domain) = <normalized>` predicate already
excludes rows that the in-memory `normalizeSSODomain(...) === domain` recheck
claimed to catch, making that recheck dead/misleading code. Match on the
canonical lower-cased domain and filter purely by ownership. Malformed legacy
values (wildcards, schemes, ports) never match an email domain at sign-in, so
excluding them is not a gap. Test DB mock now applies the lower() predicate so
the casing-variant case is genuinely exercised.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/webhooks/deploy.ts Outdated
findConflictingWebhookPathOwner omitted the isActive filter that the
runtime dispatcher (findAllWebhooksForPath) applies, so an inactive but
non-archived webhook from another workflow (e.g. after undeploy or
failure auto-disable) would permanently block any new deployment on that
path even though it never receives deliveries. Align the guard with the
runtime isActive + archivedAt filter; the earliest-owner runtime check
remains the authoritative cross-tenant protection. Also trims verbose
TSDoc on the webhook path-isolation helpers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/webhooks/deploy.ts Outdated
…nflict

findConflictingWebhookPathOwner now joins workflow and filters
isNull(workflow.archivedAt), matching the runtime dispatcher
(findAllWebhooksForPath). A webhook on an archived workflow can never
receive deliveries at runtime, so it must not block legitimate path reuse
with a 409.

Co-Authored-By: Claude Opus 4.8 <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 237be12. Configure here.

…tate

A KB file's owner is now the earliest document referencing its key regardless of
state (active/archived/deleted/excluded); access is granted only when that owning
document is still active. Closes the residual where an attacker could plant an
active document to claim a file whose original document was archived or deleted.
Reverts the knowledge-base file-access work (origin-pinning / owner-pinning /
origin allow-list in verifyKBFileAccess) and its test. The other hardening fixes
(SSO domain registration, webhook path isolation, workspace env secrets, CSV
export) are unchanged. apps/sim/app/api/files/authorization.ts is restored to its
origin/staging baseline.
…flict check

Self-hosters often register SSO user-scoped via the CLI script (no
SSO_ORGANIZATION_ID). If they later enable organizations and reconfigure the
same domain org-scoped through the UI, the conflict check previously treated
their own user-scoped row as another tenant's and returned a misleading 409.
Recognize the caller's own user-scoped provider as owned so that migration is
allowed, while still blocking another user's or another org's domain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@waleedlatif1 waleedlatif1 changed the title fix(security): harden KB file access, SSO domain registration, webhook path isolation, env secrets, and CSV export fix(security): harden SSO domain registration, webhook path isolation, env secrets, and CSV export May 30, 2026
Defer to a credential-based access model (separate change). Restores
GET /api/workspaces/[id]/environment to main behavior and removes the test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@waleedlatif1 waleedlatif1 changed the title fix(security): harden SSO domain registration, webhook path isolation, env secrets, and CSV export fix(security): harden SSO domain registration, webhook path isolation, and CSV export May 31, 2026
… helper

Extract findConflictingWebhookPathOwner to lib/webhooks/utils.server.ts as
the single source of truth for cross-tenant path-collision detection, used by
both webhook creation paths (deploy sync and the manual /api/webhooks route).

This also repairs two latent issues in the manual route's previous inline
check, which queried with limit(1) and only webhook.archivedAt:
- limit(1) inspected one arbitrary row, so a same-workflow row could mask a
  foreign collision (false negative). The shared helper scans all matching
  rows.
- It omitted isActive/workflow.archivedAt, so inactive or archived-workflow
  webhooks (which never receive deliveries) permanently blocked path reuse.
  The helper mirrors the runtime dispatcher's filter.

Same-workflow webhook reuse for upsert is now a separate, explicit lookup.

Co-Authored-By: Claude Opus 4.8 <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 9119b6a. Configure here.

@waleedlatif1 waleedlatif1 merged commit a8f86c0 into staging May 31, 2026
14 checks passed
@waleedlatif1 waleedlatif1 deleted the waleedlatif1/verify-code-changes branch May 31, 2026 00:36
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