fix: stop webhook redirect SSRF#1803
Conversation
📝 WalkthroughWalkthroughModified webhook delivery to handle HTTP redirects manually by adding Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
Comment |
There was a problem hiding this comment.
🧹 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 onglobalThis.fetchinside 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.stubGlobalwith 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
📒 Files selected for processing (2)
supabase/functions/_backend/utils/webhook.tstests/webhook-delivery-redirect.unit.test.ts
|
* 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



Summary (AI generated)
deliverWebhook()usesredirect: 'manual'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.tsenv TMPDIR=/tmp BUN_INSTALL_CACHE_DIR=/tmp/bun-cache XDG_CACHE_HOME=/tmp bunx vitest run tests/webhook-delivery-redirect.unit.test.tsenv TMPDIR=/tmp BUN_INSTALL_CACHE_DIR=/tmp/bun-cache XDG_CACHE_HOME=/tmp bun run supabase:startenv TMPDIR=/tmp BUN_INSTALL_CACHE_DIR=/tmp/bun-cache XDG_CACHE_HOME=/tmp bun run test:allGenerated with AI
Summary by CodeRabbit
Bug Fixes
Tests