Skip to content

fix(webhooks): validate and pin EmailBison apiBaseUrl before outbound requests#5415

Merged
waleedlatif1 merged 2 commits into
stagingfrom
fix-emailbison-ssrf
Jul 4, 2026
Merged

fix(webhooks): validate and pin EmailBison apiBaseUrl before outbound requests#5415
waleedlatif1 merged 2 commits into
stagingfrom
fix-emailbison-ssrf

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Email Bison webhook create/delete requests now go through validateUrlWithDNS + secureFetchWithPinnedIP for the user-configured instance URL, matching the pattern already used by the Microsoft Teams and Slack webhook providers.
  • The existing HTTPS-only check in normalizeEmailBisonBaseUrl is unchanged; this adds the DNS/IP validation step before the request is dispatched.

Test plan

  • bun test (vitest) for apps/sim/lib/webhooks/providers/emailbison.test.ts — new tests cover create/delete rejecting a blocked-address instance URL before any request is sent, and succeeding for a normal public HTTPS instance URL
  • bun run type-check (apps/sim) — clean
  • bunx biome check on touched files — clean
  • bun run check:api-validation — passed

… requests

Route Email Bison webhook create/delete requests through the shared
DNS-validated, IP-pinned fetch used by the Teams and Slack webhook
providers instead of a raw fetch to the user-configured instance URL.
@vercel

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 4, 2026 7:13pm

Request Review

@cursor

cursor Bot commented Jul 4, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Hardens SSRF-prone outbound calls using established DNS/IP validation and pinned fetch; behavior change is limited to subscription lifecycle, but mis-validation could block legitimate instances.

Overview
Email Bison webhook create and delete no longer call plain fetch against the user-supplied instance URL. Both paths now run validateUrlWithDNS on the target URL and issue the request via secureFetchWithPinnedIP with the resolved IP, aligning with other webhook providers (e.g. Microsoft Teams).

Invalid or blocked addresses fail before any outbound call; create throws a clear validation error. Delete respects strict: non-strict mode returns quietly after logging; strict mode throws. Delete cleanup uses an AlreadyLoggedError so site-specific warnings are not logged again in the outer catch.

New emailbison.test.ts covers blocked URLs (no fetch), strict vs non-strict delete, and successful create/delete with pinned fetch.

Reviewed by Cursor Bugbot for commit 742ae70. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds SSRF protection to the EmailBison webhook provider by routing both createSubscription and deleteSubscription outbound requests through validateUrlWithDNS + secureFetchWithPinnedIP, matching the guard already present in the Microsoft Teams and Slack providers. It also introduces a private AlreadyLoggedError marker class that prevents the double-warning previously emitted when the deleteSubscription try/catch wrapper caught an already-logged failure.

  • DNS + IP pinning on create: targetUrl is resolved once via validateUrlWithDNS, the returned IP is validated against private/reserved ranges, and secureFetchWithPinnedIP uses that pinned IP for the POST — preventing DNS rebinding attacks on user-supplied apiBaseUrl values.
  • Consistent guard on delete: Same DNS-validation pattern applied before the DELETE request; strict-mode failures throw AlreadyLoggedError so the outer catch skips its generic warning but still re-throws for the caller.
  • Tests: Four new unit tests verify blocked-address rejection (non-strict and strict delete) and successful create/delete flows through the mock layer.

Confidence Score: 5/5

Safe to merge — the change adds a well-established SSRF guard and fixes a pre-existing double-log edge case with no regressions to existing behavior.

The DNS validation and IP-pinning pattern matches the implementation already used for Teams and Slack webhooks, the non-null assertion on resolvedIP is safe because validateUrlWithDNS always populates it on a valid result, AlreadyLoggedError correctly isolates the catch boundary without leaking internal type details to callers, and the new tests cover both the happy path and the blocked-address rejection in both strict and non-strict modes.

No files require special attention.

Important Files Changed

Filename Overview
apps/sim/lib/webhooks/providers/emailbison.ts Adds validateUrlWithDNS + secureFetchWithPinnedIP to createSubscription and deleteSubscription; introduces AlreadyLoggedError to prevent double-logging in the delete path's try/catch wrapper.
apps/sim/lib/webhooks/providers/emailbison.test.ts New test suite covering blocked-address rejection (non-strict and strict) and successful create/delete flows with the new DNS/IP validation layer; includes afterEach cleanup for NEXT_PUBLIC_APP_URL.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller
    participant emailBisonHandler
    participant validateUrlWithDNS
    participant DNS
    participant secureFetchWithPinnedIP
    participant EmailBisonAPI

    Caller->>emailBisonHandler: createSubscription(ctx)
    emailBisonHandler->>emailBisonHandler: "emailBisonUrl('/api/webhook-url', {}, apiBaseUrl) → targetUrl"
    emailBisonHandler->>validateUrlWithDNS: validateUrlWithDNS(targetUrl, 'apiBaseUrl')
    validateUrlWithDNS->>DNS: dns.lookup(hostname)
    DNS-->>validateUrlWithDNS: resolvedIP
    validateUrlWithDNS->>validateUrlWithDNS: isPrivateOrReservedIP(resolvedIP)?
    alt blocked IP
        validateUrlWithDNS-->>emailBisonHandler: "{isValid: false, error}"
        emailBisonHandler->>emailBisonHandler: logger.warn + throw Error
        emailBisonHandler-->>Caller: throws 'Instance URL could not be validated'
    else public IP
        validateUrlWithDNS-->>emailBisonHandler: "{isValid: true, resolvedIP}"
        emailBisonHandler->>secureFetchWithPinnedIP: "secureFetchWithPinnedIP(targetUrl, resolvedIP, {POST})"
        secureFetchWithPinnedIP->>EmailBisonAPI: HTTP POST (pinned to resolvedIP)
        EmailBisonAPI-->>secureFetchWithPinnedIP: response
        secureFetchWithPinnedIP-->>emailBisonHandler: SecureFetchResponse
        emailBisonHandler-->>Caller: "{providerConfigUpdates: {externalId}}"
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Caller
    participant emailBisonHandler
    participant validateUrlWithDNS
    participant DNS
    participant secureFetchWithPinnedIP
    participant EmailBisonAPI

    Caller->>emailBisonHandler: createSubscription(ctx)
    emailBisonHandler->>emailBisonHandler: "emailBisonUrl('/api/webhook-url', {}, apiBaseUrl) → targetUrl"
    emailBisonHandler->>validateUrlWithDNS: validateUrlWithDNS(targetUrl, 'apiBaseUrl')
    validateUrlWithDNS->>DNS: dns.lookup(hostname)
    DNS-->>validateUrlWithDNS: resolvedIP
    validateUrlWithDNS->>validateUrlWithDNS: isPrivateOrReservedIP(resolvedIP)?
    alt blocked IP
        validateUrlWithDNS-->>emailBisonHandler: "{isValid: false, error}"
        emailBisonHandler->>emailBisonHandler: logger.warn + throw Error
        emailBisonHandler-->>Caller: throws 'Instance URL could not be validated'
    else public IP
        validateUrlWithDNS-->>emailBisonHandler: "{isValid: true, resolvedIP}"
        emailBisonHandler->>secureFetchWithPinnedIP: "secureFetchWithPinnedIP(targetUrl, resolvedIP, {POST})"
        secureFetchWithPinnedIP->>EmailBisonAPI: HTTP POST (pinned to resolvedIP)
        EmailBisonAPI-->>secureFetchWithPinnedIP: response
        secureFetchWithPinnedIP-->>emailBisonHandler: SecureFetchResponse
        emailBisonHandler-->>Caller: "{providerConfigUpdates: {externalId}}"
    end
Loading

Reviews (3): Last reviewed commit: "fix(webhooks): restore NEXT_PUBLIC_APP_U..." | Re-trigger Greptile

Comment thread apps/sim/lib/webhooks/providers/emailbison.test.ts
Comment thread apps/sim/lib/webhooks/providers/emailbison.ts
…e strict-delete warning log

Test env var mutation was never restored, risking cross-file leakage in
single-threaded vitest runs. Strict-mode deleteSubscription failures were
logged twice (once at the throw site with context, once generically by the
outer catch); the outer catch now skips its own log for errors already
logged at the throw site.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

1 similar comment
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 742ae70. Configure here.

@waleedlatif1 waleedlatif1 merged commit 41fb863 into staging Jul 4, 2026
18 checks passed
@waleedlatif1 waleedlatif1 deleted the fix-emailbison-ssrf branch July 4, 2026 22:37
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