Skip to content

fix(security): SSRF pinning, Twilio webhook auth, copilot token leak, audit-log tenant scoping#4899

Merged
waleedlatif1 merged 10 commits into
stagingfrom
fix/sec-t
Jun 6, 2026
Merged

fix(security): SSRF pinning, Twilio webhook auth, copilot token leak, audit-log tenant scoping#4899
waleedlatif1 merged 10 commits into
stagingfrom
fix/sec-t

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

Summary

  • ClickHouse + MCP auth-probe: pin outbound HTTP to the validated IP to close DNS-rebinding/SSRF windows
  • KB connectors (S3, GitLab, Sentry, Obsidian): route user-controlled hosts through DNS-validated, IP-pinned fetch
  • Twilio SMS webhook: enforce X-Twilio-Signature verification (was unauthenticated; forged inbound SMS could queue executions)
  • Copilot credentials API: stop returning plaintext OAuth access tokens; response now exposes only masked display metadata
  • Enterprise audit logs: scope access to the organization boundary, closing a cross-tenant IDOR
  • Added regression tests across all of the above

Type of Change

  • Bug fix (security)

Testing

Tested manually and with added unit tests (clickhouse 6, mcp probe 5, copilot credentials 4, twilio 12, connectors 37, audit-logs 15 — all passing)

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)

…binding)

clickhouseRequest() validated config.host via validateDatabaseHost() but
discarded the resolved IP and called fetch() with the original hostname,
triggering a second DNS lookup. A workflow author controlling the host
parameter could use DNS rebinding to pass validation against a public IP
and then connect to an internal/private address (SSRF).

Replace fetch() with secureFetchWithPinnedIP(), connecting to the
validated resolvedIP while preserving the hostname for Host/TLS SNI — the
same DNS-pinning pattern used by the other DB tools. Set Content-Length
explicitly so request framing is identical to the previous fetch.

Add tests locking the contract: connection targets the validated IP not
the hostname, no request is issued on validation failure, http/https and
allowHttp are selected from secure, and body/headers propagate.
…ding window

The MCP auth-type probe (detectMcpAuthType) issued raw, unpinned fetch()
calls against the user-supplied server URL, re-resolving DNS independently
of validateMcpServerSsrf. This re-opened the exact DNS-rebinding (TOCTOU)
window the pinned McpClient path was built to close: a hostname that
resolves to a public IP during validation could resolve to an internal IP
during the probe.

The probe now pins to the IP already validated by the caller via
createMcpPinnedFetch(resolvedIP); when no pre-validated IP is available it
falls back to createSsrfGuardedMcpFetch(), which validates and pins each
request. The best-effort session-close DELETE reuses the same pinned fetch.
Both call sites (test-connection route and performCreateMcpServer) thread
the resolved IP into the probe.
…lot credentials

GET /api/copilot/credentials returned each connected account's live,
post-refresh OAuth access token in plaintext to any session for that
user. The endpoint is only used for credential display/masking and no
client reads the token, so drop accessToken from the get_credentials
tool output and the copilot credentials response contract. Also removes
the incidental refreshTokenIfNeeded side-effect on this read path.

Adds regression tests:
- get-credentials: asserts the response exposes only masked metadata and
  never leaks the access/refresh token.
- revoke: locks in that revokeMcpOauthTokens routes OAuth discovery and
  RFC 7009 revocation through the SSRF-guarded fetch (no raw fetch to an
  attacker-controlled revocation_endpoint).
The twilio (SMS) provider handler implemented no verifyAuth, so the webhook
dispatcher queued workflow executions for any request to a known SMS trigger
path without validating the Twilio signature — allowing forged inbound SMS
events. Only the twilio-voice handler performed signature verification.

Extract the shared HMAC-SHA1 signature validation into twilio-signature.ts
and wire it into both the SMS and Voice handlers. Verification is enforced
when an auth token is configured (parity with Voice); requests without a
configured token pass through per the provider-wide optional-secret
convention. Add regression tests for both handlers.
…lidated, IP-pinned fetch

Knowledge connectors that accept a custom service host/endpoint (S3-compatible
endpoints, self-managed GitLab/Sentry hosts, Obsidian vault URLs) performed
server-side fetches without the repository's SSRF guard, letting an authenticated
user with KB write access probe internal/loopback hosts from the backend.

Add secureFetchWithRetry (validateUrlWithDNS + secureFetchWithPinnedIP + the same
retry/backoff as fetchWithRetry) and route every request in the s3, gitlab, sentry,
and obsidian connectors through it - including pagination and hydration. Gate the
S3 plain-http loopback exception to self-hosted deployments.
…undary

Actor membership was used as a standalone tenant predicate, letting org
admins read members' audit activity from personal workspaces and other
tenants. Scope queries to org-attached workspaces plus org-level events,
with actor membership only narrowing the scope; validate workspaceId
filters against the caller's organization.
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 6, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jun 6, 2026 8:34pm

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented Jun 6, 2026

PR Summary

High Risk
Touches authentication (Twilio webhooks, OAuth token exposure), tenant isolation (audit logs), and outbound networking (SSRF) across MCP, ClickHouse, and connectors—high impact if regressions slip through.

Overview
This PR hardens several security-sensitive paths and adds regression tests.

SSRF / DNS rebinding: ClickHouse tool HTTP calls now use secureFetchWithPinnedIP after host validation instead of plain fetch. MCP auth probing and test-connection pass the pre-validated IP into detectMcpAuthType; server lifecycle threads that IP through registration. Knowledge connectors (GitLab, Obsidian, S3, Sentry) switch from fetchWithRetry to a new secureFetchWithRetry that re-validates and pins each attempt. S3 tightens plain http:// loopback endpoints to self-hosted only. Turbopack browser aliases stub dns so client bundles don’t pull Node-only SSRF code.

Enterprise audit logs: Tenant scope is rebuilt around organizationId: workspaces load via workspace.organizationId, and buildOrgScopeCondition matches org workspaces plus org-level rows (null workspaceId with metadata or organization resource)—actor membership is no longer a standalone boundary. List/detail and session audit routes share this; v1 list rejects actorId / workspaceId filters outside the org.

Twilio webhooks: SMS and Voice handlers delegate to shared verifyTwilioAuth with X-Twilio-Signature verification when an auth token is configured (including forwarded public URL reconstruction).

Copilot credentials: The get_credentials tool and API contract drop OAuth accessToken from responses; only display metadata is returned (no refresh path in the tool).

Other: Slack OAuth typing fix for tokens.raw; Twilio signature module extracted with tests.

Reviewed by Cursor Bugbot for commit 69b3857. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 6, 2026

Greptile Summary

This PR closes five distinct security vulnerabilities across the Sim platform: SSRF/DNS-rebinding windows in ClickHouse and MCP auth-probe requests (fixed by IP-pinning post-validation), unauthenticated Twilio SMS webhooks (now enforced with HMAC-SHA1 signature verification), plaintext OAuth access-token exposure in the Copilot credentials API (response now strips all token fields), and a cross-tenant IDOR in enterprise audit logs (scope now anchored to workspace.organizationId rather than member ownership).

  • SSRF / DNS-rebinding: All user-controlled outbound fetches in KB connectors (S3, GitLab, Sentry, Obsidian), ClickHouse, and MCP OAuth probe now go through secureFetchWithPinnedIP / secureFetchWithValidation, pinning the TCP connection to the IP that passed the initial SSRF check so a renamed DNS entry cannot redirect the second hop.
  • Twilio webhook auth: A shared verifyTwilioAuth helper (HMAC-SHA1 over the reconstructed callback URL + sorted POST params) is now called by both SMS and Voice handlers; previously the SMS handler had no verifyAuth at all.
  • Audit log tenant scope: getOrgWorkspaceIds now queries workspace.organizationId (not member ownership), and buildOrgScopeCondition builds its predicate from workspace membership + org-level metadata rather than actor identity, closing the cross-tenant read path.

Confidence Score: 5/5

All five targeted vulnerabilities are correctly addressed with defense-in-depth layering, and each fix is covered by regression tests.

Every changed code path has been verified: IP-pinning is correctly threaded from SSRF validation to the actual TCP connection in ClickHouse, MCP probe, and all four KB connectors. The Twilio HMAC-SHA1 implementation matches the documented algorithm and uses constant-time comparison. The audit-log scope is now anchored to the organization FK rather than member ownership, closing the cross-tenant read path. The copilot credentials tool no longer touches or returns OAuth tokens. No regressions were found in related code paths.

No files require special attention — the changes are well-scoped, consistently applied, and backed by unit tests covering the security-critical paths.

Important Files Changed

Filename Overview
apps/sim/lib/webhooks/providers/twilio-signature.ts New shared HMAC-SHA1 Twilio signature verification helper used by both SMS and Voice handlers; correctly reconstructs the callback URL from forwarding headers and uses constant-time comparison.
apps/sim/app/api/v1/audit-logs/query.ts Audit log scope now anchored to org workspace IDs (via organizationId FK) plus org-level metadata rows; actor-membership is only a narrowing filter, closing the cross-tenant IDOR.
apps/sim/lib/knowledge/documents/secure-fetch.server.ts New server-only wrapper that threads full SSRF validation (DNS resolve → private-IP check → IP-pinned connection) through the existing retry/backoff infrastructure for KB connectors.
apps/sim/lib/mcp/oauth/probe.ts MCP auth-probe now accepts a pre-validated resolvedIP and uses a pinned fetch for both the POST probe and the DELETE session-cleanup hop, closing the DNS-rebinding gap between SSRF validation and the actual request.
apps/sim/lib/copilot/tools/server/user/get-credentials.ts Removes accessToken (and the opportunistic refresh call) from the credentials response; now returns only display metadata (id, name, provider, serviceName, lastUsed, isDefault).
apps/sim/app/api/tools/clickhouse/utils.ts ClickHouse HTTP requests now route through secureFetchWithPinnedIP using the IP resolved during host validation, preventing DNS rebinding between validation and the actual fetch.
apps/sim/connectors/s3/s3.ts S3 connector switched from fetchWithRetry to secureFetchWithRetry; http-loopback allowance is now additionally gated on !isHosted so hosted deployments cannot reach MinIO-style loopback targets.
apps/sim/app/api/v1/audit-logs/route.ts Adds an early 400 guard that rejects workspaceId filter values outside the caller's org workspace list; scope condition rebuilt from org context rather than actor membership.
apps/sim/app/api/v1/audit-logs/[id]/route.ts Single-record lookup now applies the same org-boundary scope condition as the list endpoint, replacing the previous unawaited Drizzle subquery with an explicit awaited ID list.
apps/sim/lib/mcp/orchestration/server-lifecycle.ts validateMcpServerUrl now returns the resolved IP alongside the ok/error result so it can be threaded into detectMcpAuthType, pinning the auth probe to the same IP that passed SSRF validation.

Reviews (2): Last reviewed commit: "fix(build): keep connector SSRF fetch ou..." | Re-trigger Greptile

Comment thread apps/sim/lib/webhooks/providers/twilio-signature.ts
Comment thread apps/sim/lib/webhooks/providers/twilio-signature.ts
Comment thread apps/sim/app/api/v1/audit-logs/query.ts Outdated
Addresses PR review: when no auth token is set, verifyTwilioAuth skips
signature verification (optional-secret convention). Log a warning so
operators can detect a webhook running unauthenticated.
…dit scope

SQL IN never matches NULL, so system/automated events inside org
workspaces were hidden unless includeDeparted=true. The default scope now
matches current members OR null-actor rows, still inside the org boundary.
Installed better-auth's OAuth2Tokens no longer declares the raw property;
access it through an intersection cast (no behavior change) so type-check
passes.
The connectors SSRF fix routed s3/gitlab/sentry/obsidian through
secureFetchWithRetry, which transitively imports input-validation.server
(and its Node-only `dns/promises`). connectors/registry.ts is imported by
client components for connector metadata, so the connector sync code —
which only ever runs in server API routes — gets pulled into the client
bundle, and Turbopack fails to resolve `dns/promises` (no browser shim).

- Move secureFetchWithRetry into a dedicated `secure-fetch.server` module so
  the shared documents/utils stays client-safe; connectors import from there.
- Add a browser-only `turbopack.resolveAlias` stub for `dns`/`dns/promises`
  (the documented Next 16 remedy). Server bundles keep the real module, so
  SSRF validation is unaffected — only the never-executed client copy is stubbed.

Verified with a full `next build` (compiles successfully, no module errors).
@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 69b3857. Configure here.

@waleedlatif1 waleedlatif1 merged commit 20a00a1 into staging Jun 6, 2026
14 checks passed
@waleedlatif1 waleedlatif1 deleted the fix/sec-t branch June 6, 2026 22:13
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