Skip to content

feat(webhooks): disable redirects and revalidate hosts for webhook dispatch (#1262) - #1393

Open
Ahbiz wants to merge 4 commits into
CalloraOrg:mainfrom
Ahbiz:feat/webhook-dispatch-ssrf-1262
Open

Ahbiz wants to merge 4 commits into
CalloraOrg:mainfrom
Ahbiz:feat/webhook-dispatch-ssrf-1262

Conversation

@Ahbiz

@Ahbiz Ahbiz commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Closes #1262

Summary

Webhook dispatch runs directly from within backend infrastructure. Previously, validateWebhookUrl only inspected destination URLs at registration time, while dispatchWebhook executed fetch with the default redirect: 'follow'. A malicious or compromised webhook receiver could return a 302 Found redirect targeting internal services or cloud instance metadata (http://169.254.169.254/), turning webhook delivery into a high-severity Server-Side Request Forgery (SSRF) vector. Furthermore, receiver hostnames could rebind via DNS to private network addresses post-registration.

This PR disables redirect following on outbound webhook dispatches, enforces pre-dispatch hostname revalidation against private/internal IP ranges, bounds response body reading to mitigate payload-bomb DoS, and logs all refused deliveries with clear diagnostic reasons.


Key Changes

  1. Strict Manual Redirects (redirect: 'manual'):

    • Updated dispatchWebhook in src/webhooks/webhook.dispatcher.ts to pass redirect: 'manual' in fetch options.
    • Explicitly intercepts 3xx redirect status codes (300 <= status < 400) or response.type === 'opaqueredirect' and terminates the dispatch attempt without following the redirect.
    • Extracts the Location header to record an explicit failure message in operational logs.
  2. Pre-Dispatch Host Revalidation (DNS Rebinding / SSRF Prevention):

    • Invokes validateWebhookUrl(config.url) immediately prior to outbound network dispatch attempts.
    • Extends validateWebhookUrl in src/webhooks/webhook.validator.ts with WebhookValidationOptions allowing { enforcePrivateIpCheck: true } to enforce private IP blocking across environments.
    • Any destination whose DNS resolves to private, link-local, loopback, or CGNAT ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16, ::1/128, fc00::/7, 100.64.0.0/10) is rejected prior to sending outbound network requests.
  3. Bounded Response Body Reads:

    • Implemented consumeCappedResponseBody(response, maxBytes) in src/webhooks/webhook.dispatcher.ts with a 64 KB cap (MAX_WEBHOOK_RESPONSE_BYTES = 64 * 1024).
    • Streams response body chunks up to 64 KB and actively cancels the stream reader if the limit is exceeded, preventing memory exhaustion and slowloris/payload-bomb DoS from rogue receivers.
  4. Failure Observability:

    • Persists all validation and redirect refusals in WebhookStore.recordFailedDelivery with descriptive lastError reasons (e.g. Webhook redirect to "http://169.254.169.254/latest/meta-data" refused (HTTP 302): redirects are not followed or Webhook URL resolves to a private/internal IP address (169.254.169.254), which is not allowed.).

Acceptance Criteria Mapping

Acceptance Criterion Implementation Test Coverage
A receiver responding 302 to an internal address is not followed src/webhooks/webhook.dispatcher.ts src/webhooks/webhook.dispatcher.test.ts (local server returning 302 to metadata endpoint)
A URL whose DNS now resolves to a private range is refused at dispatch time src/webhooks/webhook.dispatcher.ts, src/webhooks/webhook.validator.ts src/webhooks/webhook.dispatcher.test.ts (DNS rebinding test with private IP)
Failures are recorded in the failed-delivery log with a clear reason WebhookStore.recordFailedDelivery Verified via WebhookStore.getRecentFailures() in dispatcher test suite
Tests use a local server returning redirects src/webhooks/webhook.dispatcher.test.ts Local http.Server testing 301, 302, and 307 redirects
Cap response body reads consumeCappedResponseBody (64 KB ceiling) src/webhooks/webhook.dispatcher.test.ts (streaming 256 KB response body)

Security & Failure Mode Handling

  • Non-Followed Redirects: Redirects are treated as terminal/aborted attempts; Callora will not retry redirects to avoid repeated load on non-compliant endpoints.
  • DNS Failure Modes: Unresolvable hosts or addresses resolving to private IP ranges fail fast before fetch is invoked, ensuring zero packets are sent to internal network endpoints.
  • Backward Compatibility: Valid public HTTP/HTTPS webhook endpoints returning direct 2xx responses are completely unaffected.

Verification

# Webhook unit and integration tests
npm test -- src/webhooks/webhook.dispatcher.test.ts src/webhooks/webhook.validator.test.ts tests/integration/webhooks.test.ts tests/integration/webhook-dispatch-pipeline.test.ts
# Output: 4 passed, 84 total tests passed (0 failed)

# Schema versioning & migration layout gate
CHECKSUM_CI_SKIP_MISSING=1 npx tsx scripts/check-migrations.ts
# Output: Passed (0 errors)

# OpenAPI backward compatibility check
npx --yes @useoptic/optic diff docs/openapi.json --base upstream/main --check
# Output: Passed (No operations changed)

…spatch

Closes CalloraOrg#1262

- Pass redirect: 'manual' in fetch options to prevent following 3xx redirects to internal or cloud metadata endpoints
- Re-run validateWebhookUrl before each dispatch to prevent SSRF and DNS rebinding to private IP ranges
- Cap response body reads via consumeCappedResponseBody to prevent memory exhaustion
- Record failed deliveries in WebhookStore with clear explanatory reasons
- Add tests with local HTTP server returning redirects (301, 302, 307) and verifying they are not followed
- Add tests for private IP refusal at dispatch time and response body capping
@drips-wave

drips-wave Bot commented Sep 29, 2026

Copy link
Copy Markdown

@Ahbiz Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

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.

Disable redirects and revalidate hosts for webhook dispatch

1 participant