Skip to content

fix: stop webhook redirect SSRF#1803

Merged
riderx merged 4 commits into
mainfrom
fix/webhook-manual-redirect-ghsa-72qg
Mar 17, 2026
Merged

fix: stop webhook redirect SSRF#1803
riderx merged 4 commits into
mainfrom
fix/webhook-manual-redirect-ghsa-72qg

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Mar 16, 2026

Summary (AI generated)

  • stop webhook deliveries from automatically following HTTP redirects
  • add a unit regression test that asserts deliverWebhook() uses redirect: 'manual'
  • preserve existing webhook URL validation while closing the redirect-based SSRF gap

Motivation (AI generated)

Webhook URLs were validated only for the initial target. The delivery fetch still followed server-provided redirects automatically, which allowed a validated public HTTPS endpoint to bounce requests toward internal resources.

Business Impact (AI generated)

This closes a high-severity webhook SSRF path that could expose internal services or metadata endpoints and leak response data through webhook delivery records. Reducing that risk protects tenant trust and avoids incident response overhead.

Test Plan (AI generated)

  • env TMPDIR=/tmp BUN_INSTALL_CACHE_DIR=/tmp/bun-cache XDG_CACHE_HOME=/tmp bunx eslint supabase/functions/_backend/utils/webhook.ts tests/webhook-delivery-redirect.unit.test.ts
  • env TMPDIR=/tmp BUN_INSTALL_CACHE_DIR=/tmp/bun-cache XDG_CACHE_HOME=/tmp bunx vitest run tests/webhook-delivery-redirect.unit.test.ts
  • env TMPDIR=/tmp BUN_INSTALL_CACHE_DIR=/tmp/bun-cache XDG_CACHE_HOME=/tmp bun run supabase:start
  • env TMPDIR=/tmp BUN_INSTALL_CACHE_DIR=/tmp/bun-cache XDG_CACHE_HOME=/tmp bun run test:all

Generated with AI

Summary by CodeRabbit

  • Bug Fixes

    • Improved webhook delivery handling to manually process HTTP redirects instead of automatically following them, preventing unwanted request routing to unexpected endpoints.
  • Tests

    • Added test coverage validating manual redirect handling behavior in webhook delivery operations.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 16, 2026

📝 Walkthrough

Walkthrough

Modified webhook delivery to handle HTTP redirects manually by adding redirect: 'manual' to the fetch configuration, ensuring redirects are not automatically followed. Added a corresponding unit test to verify the manual redirect behavior returns success: false with the 302 status.

Changes

Cohort / File(s) Summary
Webhook Manual Redirect Handling
supabase/functions/_backend/utils/webhook.ts
Added redirect: 'manual' parameter to fetch options in deliverWebhook, preventing automatic redirect following.
Webhook Redirect Test Coverage
tests/webhook-delivery-redirect.unit.test.ts
New unit test file verifying that webhook delivery correctly handles manual redirects, asserting success: false and 302 status response.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related PRs

Poem

🐰 A redirect came bouncing down the line,
"No auto-follow!" the webhook did opine,
With manual: true, it holds its ground,
Tests verify redirects won't silently be found! 🔄

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: stop webhook redirect SSRF' clearly and concisely describes the primary change: preventing SSRF attacks via webhook redirects.
Description check ✅ Passed The PR description covers the main summary, motivation, business impact, and a detailed test plan. However, it omits the optional screenshots section and has incomplete checklist items.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/webhook-manual-redirect-ghsa-72qg
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

@riderx riderx added the codex label Mar 16, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/webhook-delivery-redirect.unit.test.ts (1)

52-80: Consider isolating the fetch spy for concurrent test safety.

The test correctly validates the redirect: 'manual' fix, but spying on globalThis.fetch inside a concurrent test could cause interference with other concurrent tests that use fetch. Since this file currently has only one test, it's not immediately problematic, but it's fragile for future additions.

Consider wrapping the spy in the test body with explicit cleanup, or use vi.stubGlobal with per-test isolation if more fetch-dependent tests are added later.

💡 Optional: Explicit cleanup within the test
   it.concurrent('uses manual redirect mode for outbound webhook delivery', async () => {
     const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('redirect blocked', {
       status: 302,
       headers: {
         location: 'http://169.254.169.254/latest/meta-data',
       },
     }))
 
+    try {
       const result = await deliverWebhook(
         context as any,
         'delivery-123',
         'https://example.com/webhook',
         payload,
         'whsec_test_secret',
       )
 
       expect(fetchMock).toHaveBeenCalledWith(
         'https://example.com/webhook',
         expect.objectContaining({
           method: 'POST',
           redirect: 'manual',
         }),
       )
       expect(result).toMatchObject({
         success: false,
         status: 302,
         body: 'redirect blocked',
       })
+    }
+    finally {
+      fetchMock.mockRestore()
+    }
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/webhook-delivery-redirect.unit.test.ts` around lines 52 - 80, The test
currently spies on globalThis.fetch which can leak into other concurrent tests;
replace the vi.spyOn(globalThis, 'fetch') usage with a per-test stub (e.g.,
vi.stubGlobal('fetch', mockedFetch)) or ensure explicit cleanup by calling
fetchMock.mockRestore() (or vi.restoreAllMocks()) in an afterEach/ finally block
so the stub is removed; update the test that calls deliverWebhook to use the
stubbed fetch and add per-test teardown to prevent interference with other
tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/webhook-delivery-redirect.unit.test.ts`:
- Around line 52-80: The test currently spies on globalThis.fetch which can leak
into other concurrent tests; replace the vi.spyOn(globalThis, 'fetch') usage
with a per-test stub (e.g., vi.stubGlobal('fetch', mockedFetch)) or ensure
explicit cleanup by calling fetchMock.mockRestore() (or vi.restoreAllMocks()) in
an afterEach/ finally block so the stub is removed; update the test that calls
deliverWebhook to use the stubbed fetch and add per-test teardown to prevent
interference with other tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1acc7dc-18ca-4726-ab77-c0fda9303495

📥 Commits

Reviewing files that changed from the base of the PR and between e1cd9a0 and d4652ba.

📒 Files selected for processing (2)
  • supabase/functions/_backend/utils/webhook.ts
  • tests/webhook-delivery-redirect.unit.test.ts

@sonarqubecloud
Copy link
Copy Markdown

@riderx riderx merged commit 5170dd3 into main Mar 17, 2026
15 checks passed
@riderx riderx deleted the fix/webhook-manual-redirect-ghsa-72qg branch March 17, 2026 13:20
riderx added a commit that referenced this pull request Mar 19, 2026
* fix(api): harden plan usage org rpc access

* Merge branch 'main' into fix/ghsa-wh77-plan-rpc-access

* fix(tests): correct plan usage rpc test typings

* Merge branch 'main' into fix/ghsa-wh77-plan-rpc-access

* test(db): authenticate hardened plan rpc assertions

* test(db): keep plan rpc auth context through negative case

* test: cover plan usage rpc via authenticated client

* test: parallelize plan usage rpc checks

* fix: ensure FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 is set in all workflow files

* chore(release): 12.116.63

* fix(webhooks): stop following webhook redirects (#1803)

* chore(release): 12.116.64

* fix(devices): restore pagination after page 1 (#1820)

* fix(devices): normalize analytics pagination cursor timestamps

* test(devices): run cloudflare datetime checks concurrently

* chore(release): 12.116.65

* fix(sso): prevent orphan org on first login and use correct IdP URLs

- Add DB migration to skip auto-org creation in generate_org_on_user_create
  when the new user's email domain has an active SSO provider. Previously,
  auth.ts lazily creating the public.users row would trigger an unwanted
  personal org for brand-new SSO users.
- Replace client-side SP metadata URL computation in SsoConfiguration.vue
  (which used VITE_SUPABASE_URL / custom domain) with a fetch to the
  existing /private/sso/sp-metadata endpoint, which derives ACS URL and
  Entity ID from SUPABASE_URL — the same value Supabase uses internally
  for SAML processing.
- Add sp_metadata_url to sp-metadata.ts response to match the SpMetadata
  interface consumed by the frontend.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: update bun lockfile after install

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(sso): harden trigger and sp-metadata after security audit

- generate_org_on_user_create: add auth.users provider check to avoid
  suppressing personal org for email/password signups on SSO domains
  (Finding 2 — High). Email/password users on a corporate domain still
  get a personal org; provision-user.ts rejects them anyway.
- generate_org_on_user_create: add sso_enabled org flag to EXISTS query,
  matching check_domain_sso and all other SSO lookups (Finding 1 — Critical).
- generate_org_on_user_create: apply btrim to domain component to match
  the lower(btrim(domain)) normalization contract from migration
  20260312183000 (Finding 6 — Medium).
- sp-metadata.ts: add return before quickError to prevent silent
  fall-through if quickError signature ever changes (Finding 3 — High).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(sso): surface toast on SP metadata fetch failures

Both error paths in fetchSpMetadata (non-ok response and network error)
now show a toast.error alongside the existing console.error, consistent
with all other error handling in SsoConfiguration.vue.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(sso): add structured cloudlog to sp-metadata unauthorized branch

Add Context<MiddlewareKeyVariables> type annotation, cloudlog import,
and requestId to the app.get handler. The !auth branch now emits a
cloudlog entry with requestId, message, and auth value before returning
quickError, consistent with the logging pattern in provision-user.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(sso): restrict org-skip to SAML providers only in trigger

The previous logic checked has_sso AFTER the provider IF, and skipped
org creation for any non-email/phone provider when has_sso was true —
incorrectly affecting OAuth users (google, github, etc.) whose email
domain happened to match an active SSO provider.

Restructure: compute has_sso first, then use a single condition
NOT (user_provider ~ '^sso:' AND has_sso) to gate the INSERT.
Supabase sets app_metadata.provider to 'sso:<uuid>' for SAML sessions,
so OAuth providers never match '^sso:' and always get a personal org.
This also eliminates the duplicate INSERT path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(sso): merge existing users on SSO login

* fix(sso): stop editing merged migration

* fix(sso): harden SSO login and merge flow

* fix(sso): align SAML guards and Cloudflare routes

* fix(sso): sync auth flags with enforcement

* fix(sso): close shared pg pool on workerd

* fix(frontend): only send active log sort directions (#1822)

* chore(release): 12.116.66

* chore(release): 12.116.67

* chore(release): 12.116.68

* fix(db): avoid cache writes for plan usage read-only calls

* fix(cron): use writable pg client

* fix(api): keep plan usage RPC read-only

* fix(api): restore plan usage read-only guard

* fix(api): drop unused plan rpc variable

* Merge origin/main

* fix: resolve package manifest conflict

* docs(sql): add docstrings for plan usage RPCs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant