Conversation
…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
|
@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! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1262
Summary
Webhook dispatch runs directly from within backend infrastructure. Previously,
validateWebhookUrlonly inspected destination URLs at registration time, whiledispatchWebhookexecutedfetchwith the defaultredirect: 'follow'. A malicious or compromised webhook receiver could return a302 Foundredirect 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
Strict Manual Redirects (
redirect: 'manual'):dispatchWebhookinsrc/webhooks/webhook.dispatcher.tsto passredirect: 'manual'infetchoptions.300 <= status < 400) orresponse.type === 'opaqueredirect'and terminates the dispatch attempt without following the redirect.Locationheader to record an explicit failure message in operational logs.Pre-Dispatch Host Revalidation (DNS Rebinding / SSRF Prevention):
validateWebhookUrl(config.url)immediately prior to outbound network dispatch attempts.validateWebhookUrlinsrc/webhooks/webhook.validator.tswithWebhookValidationOptionsallowing{ enforcePrivateIpCheck: true }to enforce private IP blocking across environments.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.Bounded Response Body Reads:
consumeCappedResponseBody(response, maxBytes)insrc/webhooks/webhook.dispatcher.tswith a 64 KB cap (MAX_WEBHOOK_RESPONSE_BYTES = 64 * 1024).Failure Observability:
WebhookStore.recordFailedDeliverywith descriptivelastErrorreasons (e.g.Webhook redirect to "http://169.254.169.254/latest/meta-data" refused (HTTP 302): redirects are not followedorWebhook URL resolves to a private/internal IP address (169.254.169.254), which is not allowed.).Acceptance Criteria Mapping
src/webhooks/webhook.dispatcher.tssrc/webhooks/webhook.dispatcher.test.ts(local server returning 302 to metadata endpoint)src/webhooks/webhook.dispatcher.ts,src/webhooks/webhook.validator.tssrc/webhooks/webhook.dispatcher.test.ts(DNS rebinding test with private IP)WebhookStore.recordFailedDeliveryWebhookStore.getRecentFailures()in dispatcher test suitesrc/webhooks/webhook.dispatcher.test.tshttp.Servertesting 301, 302, and 307 redirectsconsumeCappedResponseBody(64 KB ceiling)src/webhooks/webhook.dispatcher.test.ts(streaming 256 KB response body)Security & Failure Mode Handling
fetchis invoked, ensuring zero packets are sent to internal network endpoints.Verification