Skip to content

fix(node): guard webhook delivery against SSRF targets - #491

Merged
pratyush618 merged 5 commits into
ByteVeda:masterfrom
stromanni:fix/node-webhook-ssrf-guard
Jul 21, 2026
Merged

fix(node): guard webhook delivery against SSRF targets#491
pratyush618 merged 5 commits into
ByteVeda:masterfrom
stromanni:fix/node-webhook-ssrf-guard

Conversation

@stromanni

@stromanni stromanni commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Closes #481.

Webhook URLs registered through the dashboard or the API were fetched as-is, so anyone able to write a subscription could point the worker at http://169.254.169.254/..., a loopback admin port, or the RFC1918 intranet.

What changed

  • src/webhooks/urlSafety.ts (new) — assertSafeWebhookUrl (resolves the host) and assertSafeWebhookUrlSync (no DNS). Rejects non-http(s) schemes, hostless URLs, local-only names (localhost, *.internal, *.local, …), and addresses in loopback / unspecified / RFC1918 / CGNAT / link-local / multicast / reserved / IPv6 unique-local ranges. IPv4-mapped IPv6 is unwrapped and judged as its v4 address.
  • RegistrationWebhookManager runs the DNS-free checks on create/update, so a literal private target is rejected with a 400 (UnsafeWebhookUrlError extends WebhookValidationError).
  • DeliveryDeliverer re-validates before every attempt, closing the registration-to-delivery DNS-rebinding window, and now uses redirect: "manual" so a 3xx cannot walk past the guard. A refused delivery is recorded as failed and is not retried.
  • BypassTASKITO_WEBHOOKS_ALLOW_PRIVATE (truthy) disables the address checks for local development, matching the cross-SDK contract. Scheme and host are still validated.

Tests

test/core/webhookUrlSafety.test.ts covers the blocked ranges and mapped IPv6 forms, local-only names, scheme/hostless rejection under bypass, falsy env values, registration refusal, a delivery blocked after the target becomes unsafe, and a refused redirect. Existing loopback-targeting webhook tests opt into the bypass.

Node suite: 72 files / 449 tests green; tsc, biome, and the docs typecheck/lint all pass.

Summary by CodeRabbit

  • Security
    • Added SSRF protection for webhook destination URLs: blocks loopback/private/link-local and cloud-metadata targets at registration and re-validates immediately before each delivery.
    • Redirects are not followed; blocked/redirected deliveries are recorded as failed (non-retryable).
    • Added a development-only override (TASKITO_WEBHOOKS_ALLOW_PRIVATE=1) for local testing.
  • Documentation
    • Rewrote webhook security guidance to reflect failed delivery status and enforced delivery rules.
  • Developer Experience
    • Exposed URL-safety utilities in the Node SDK, including the unsafe-destination error type. Added coverage for lookup timeouts.

Validates the destination at registration and before every delivery attempt, and stops following redirects, so a rebound name or a 302 cannot reach loopback, private, or metadata addresses.
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31800545-e27b-481e-81fb-f73c64cc550a

📥 Commits

Reviewing files that changed from the base of the PR and between 7769462 and ffe0841.

📒 Files selected for processing (8)
  • docs/content/docs/node/guides/operations/security.mdx
  • sdks/node/src/index.ts
  • sdks/node/src/webhooks/deliverer.ts
  • sdks/node/src/webhooks/index.ts
  • sdks/node/src/webhooks/manager.ts
  • sdks/node/src/webhooks/urlSafety.ts
  • sdks/node/test/core/webhookLookupTimeout.test.ts
  • sdks/node/test/core/webhookUrlSafety.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • sdks/node/src/webhooks/index.ts
  • sdks/node/src/webhooks/manager.ts
  • sdks/node/test/core/webhookUrlSafety.test.ts
  • sdks/node/src/webhooks/deliverer.ts

📝 Walkthrough

Walkthrough

Webhook URLs now undergo SSRF validation at registration and before delivery attempts. Unsafe destinations and redirects are not retried, delivery status includes "failed", safety APIs are exported, and documentation and tests cover the new behavior.

Changes

Webhook SSRF protection

Layer / File(s) Summary
URL safety validation
sdks/node/src/webhooks/urlSafety.ts, sdks/node/test/core/webhookLookupTimeout.test.ts
Adds synchronous and asynchronous validation for schemes, hostnames, DNS results, private and reserved IP ranges, IPv4-mapped IPv6, resolution timeouts, and development bypass configuration.
Registration validation and exports
sdks/node/src/webhooks/manager.ts, sdks/node/src/webhooks/errors.ts, sdks/node/src/webhooks/index.ts, sdks/node/src/index.ts
Webhook registration uses the safety validator, and the new validators, options types, and unsafe-URL error are publicly exported.
Delivery retry and redirect enforcement
sdks/node/src/webhooks/deliverer.ts
Destinations are re-validated before attempts, requests use an SSRF-aware Node HTTP client, redirects are not followed, and unsafe or redirected deliveries are marked non-retryable and "failed".
Safety coverage and documentation
sdks/node/test/core/webhookUrlSafety.test.ts, sdks/node/test/core/webhooks.test.ts, sdks/node/test/dashboard/webhookDeliveries.test.ts, docs/content/docs/node/guides/extensibility/webhooks.mdx, docs/content/docs/node/guides/operations/security.mdx
Tests cover unsafe destinations, bypass behavior, resolver failures and timeouts, registration, delivery re-checks, and redirects; documentation describes the guard and failed delivery state.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Deliverer
  participant URLSafety
  participant WebhookEndpoint
  Deliverer->>URLSafety: Validate destination before each attempt
  URLSafety-->>Deliverer: Allow or reject unsafe URL
  Deliverer->>WebhookEndpoint: POST with redirects disabled
  WebhookEndpoint-->>Deliverer: Response or 3xx redirect
  Deliverer-->>WebhookEndpoint: Record failed or retry outcome
Loading

Possibly related PRs

  • ByteVeda/taskito#490: Directly overlaps the Node webhook SSRF guard, redirect refusal, and delivery integration.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed It clearly summarizes the main change: adding SSRF protection for Node webhook delivery.
Linked Issues check ✅ Passed The PR ports the webhook SSRF guard to Node, rejecting unsafe destinations at registration and delivery as requested.
Out of Scope Changes check ✅ Passed The docs, tests, and runtime changes all support the SSRF guard; no unrelated scope is evident.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
docs/content/docs/node/guides/operations/security.mdx (1)

44-60: 🔒 Security & Privacy | 🔵 Trivial

Eliminate the resolve-to-connect race using a custom dispatcher.

The documentation correctly identifies the residual DNS rebinding race condition between URL validation and the socket connection. If you wish to completely eliminate this TOCTOU in Node.js in the future, you can supply a custom undici.Agent or Dispatcher to fetch. By overriding the connect or lookup function, you can validate the exact IP address immediately before the socket connects (or force the socket to use the already-validated IP from assertSafeWebhookUrl).

This eliminates the need for a secondary network egress allowlist just for the webhook worker.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/content/docs/node/guides/operations/security.mdx` around lines 44 - 60,
Update the webhook delivery documentation to describe eliminating the
resolve-to-connect race with a custom undici.Agent or Dispatcher supplied to
fetch, using its connect or lookup hook to validate or reuse the IP resolved by
assertSafeWebhookUrl immediately before socket connection. Remove the statement
that a separate egress allowlist is required solely for this race, while
retaining the strict delivery timeout guidance.
sdks/node/src/webhooks/urlSafety.ts (1)

104-109: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No timeout on the DNS lookup used for per-attempt validation.

lookup(hostname, { all: true, verbatim: true }) has no bound; an attacker-controlled or unresponsive resolver for the webhook host can stall this await well beyond typical resolver defaults, delaying (or effectively blocking) each delivery attempt before the fetch() timeout even gets a chance to apply.

♻️ Suggested fix: bound the resolution
-  let resolved: Array<{ address: string }>;
-  try {
-    resolved = await lookup(hostname, { all: true, verbatim: true });
-  } catch {
-    throw new UnsafeWebhookUrlError(`could not resolve webhook host ${hostname}`);
-  }
+  let resolved: Array<{ address: string }>;
+  try {
+    resolved = await withTimeout(lookup(hostname, { all: true, verbatim: true }), DNS_TIMEOUT_MS);
+  } catch {
+    throw new UnsafeWebhookUrlError(`could not resolve webhook host ${hostname}`);
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/node/src/webhooks/urlSafety.ts` around lines 104 - 109, Bound the DNS
resolution await in the webhook URL validation flow around lookup by applying
the existing per-attempt timeout or an appropriate resolver-specific timeout.
Ensure stalled lookups reject within that bound and continue to produce
UnsafeWebhookUrlError, while preserving the current resolved-address handling
for successful lookups.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sdks/node/src/webhooks/deliverer.ts`:
- Around line 72-76: Update the redirect-handling condition in the webhook
delivery flow to also detect opaque manual redirects by checking response.type
=== "opaqueredirect" or response.status === 0. Preserve the existing
nonRetryable behavior and redirect error handling for both standard 3xx
responses and opaque redirects.

In `@sdks/node/src/webhooks/urlSafety.ts`:
- Around line 82-113: Update the delivery transport flow using
assertSafeWebhookUrl so it connects to the validated resolved address rather
than allowing fetch(webhook.url, ...) to perform a second hostname lookup.
Propagate the selected validated address and preserve the original hostname for
HTTP Host/TLS SNI handling, ensuring private rebinding targets cannot be reached
between validation and connection.

---

Nitpick comments:
In `@docs/content/docs/node/guides/operations/security.mdx`:
- Around line 44-60: Update the webhook delivery documentation to describe
eliminating the resolve-to-connect race with a custom undici.Agent or Dispatcher
supplied to fetch, using its connect or lookup hook to validate or reuse the IP
resolved by assertSafeWebhookUrl immediately before socket connection. Remove
the statement that a separate egress allowlist is required solely for this race,
while retaining the strict delivery timeout guidance.

In `@sdks/node/src/webhooks/urlSafety.ts`:
- Around line 104-109: Bound the DNS resolution await in the webhook URL
validation flow around lookup by applying the existing per-attempt timeout or an
appropriate resolver-specific timeout. Ensure stalled lookups reject within that
bound and continue to produce UnsafeWebhookUrlError, while preserving the
current resolved-address handling for successful lookups.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2da8a985-2f91-42f9-8e5c-2ffcc13e9a3a

📥 Commits

Reviewing files that changed from the base of the PR and between d5d0e98 and d76396b.

📒 Files selected for processing (11)
  • docs/content/docs/node/guides/extensibility/webhooks.mdx
  • docs/content/docs/node/guides/operations/security.mdx
  • sdks/node/src/index.ts
  • sdks/node/src/webhooks/deliverer.ts
  • sdks/node/src/webhooks/errors.ts
  • sdks/node/src/webhooks/index.ts
  • sdks/node/src/webhooks/manager.ts
  • sdks/node/src/webhooks/urlSafety.ts
  • sdks/node/test/core/webhookUrlSafety.test.ts
  • sdks/node/test/core/webhooks.test.ts
  • sdks/node/test/dashboard/webhookDeliveries.test.ts

Comment thread sdks/node/src/webhooks/deliverer.ts
Comment thread sdks/node/src/webhooks/urlSafety.ts
Delivery now goes through node:http(s) with a validating lookup, so the socket dials the address the guard checked and a 3xx never reads as a success on runtimes that return an opaque redirect.
@pratyush618

Copy link
Copy Markdown
Collaborator

@CodeRabbit full review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sdks/node/src/webhooks/urlSafety.ts`:
- Around line 93-113: Update assertSafeWebhookUrl and the preflight handling in
deliver to preserve retryability for transient DNS failures: only convert
explicit policy refusals into UnsafeWebhookUrlError, let lookup failures
propagate as ordinary errors, and handle those plain errors in deliver as
retryable attempts with backoff while retaining the existing non-retryable path
for UnsafeWebhookUrlError.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6c65d473-e5cf-4aae-8f54-302a673b97d3

📥 Commits

Reviewing files that changed from the base of the PR and between d5d0e98 and 7769462.

📒 Files selected for processing (11)
  • docs/content/docs/node/guides/extensibility/webhooks.mdx
  • docs/content/docs/node/guides/operations/security.mdx
  • sdks/node/src/index.ts
  • sdks/node/src/webhooks/deliverer.ts
  • sdks/node/src/webhooks/errors.ts
  • sdks/node/src/webhooks/index.ts
  • sdks/node/src/webhooks/manager.ts
  • sdks/node/src/webhooks/urlSafety.ts
  • sdks/node/test/core/webhookUrlSafety.test.ts
  • sdks/node/test/core/webhooks.test.ts
  • sdks/node/test/dashboard/webhookDeliveries.test.ts

Comment thread sdks/node/src/webhooks/urlSafety.ts Outdated
The pre-attempt check resolved DNS and reported every failure as an SSRF
refusal, so one transient resolver hiccup killed the delivery with zero
attempts. The connect-time lookup already decides addresses, and cannot be
raced, so the pre-attempt check now rules only on the URL itself.
A request timeout only starts once a socket is assigned, so an unresponsive
resolver stalled delivery indefinitely. Resolution now shares the
subscription's timeout budget.
@pratyush618
pratyush618 merged commit a78b0d7 into ByteVeda:master Jul 21, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an SSRF guard to Node webhook delivery

3 participants