Skip to content

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

Closed
pratyush618 wants to merge 2 commits into
masterfrom
fix/node-webhook-ssrf-guard
Closed

fix(node): guard webhook delivery against SSRF targets#490
pratyush618 wants to merge 2 commits into
masterfrom
fix/node-webhook-ssrf-guard

Conversation

@pratyush618

@pratyush618 pratyush618 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

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

  • New Features

    • Added protection against webhook destinations targeting private, local, link-local, metadata, reserved, or multicast addresses.
    • Webhook URLs are rechecked before delivery, and redirects are not followed.
    • Added public URL-safety validation helpers for SDK users.
    • Added TASKITO_WEBHOOKS_ALLOW_PRIVATE=1 for local development.
  • Bug Fixes

    • Blocked or redirected deliveries are now recorded as failed without retries.
    • Documentation updated with webhook security requirements and delivery statuses.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 33 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3027562c-a9b1-4cae-9fef-e1fbae22ee19

📥 Commits

Reviewing files that changed from the base of the PR and between 6e57eaf 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
📝 Walkthrough

Walkthrough

Webhook URLs now undergo SSRF checks during registration and before delivery attempts. Unsafe destinations and redirects stop delivery without retries and are recorded as failed. New validators, errors, tests, exports, and security documentation cover the behavior.

Changes

Webhook SSRF protection

Layer / File(s) Summary
URL safety validation
sdks/node/src/webhooks/urlSafety.ts
Adds synchronous and asynchronous validation for URL schemes, hostnames, DNS results, and blocked IPv4 or IPv6 ranges, with a local-development bypass.
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
Applies safety validation during webhook registration and exports the new validators, options, and error type.
Delivery rechecks and refusal states
sdks/node/src/webhooks/deliverer.ts
Rechecks URLs before attempts, rejects redirects without retries, and records refused deliveries as failed.
Behavior 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
Adds coverage for unsafe destinations, bypass handling, registration, delivery, and redirects; documents the guard and delivery statuses.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WebhookManager
  participant Deliverer
  participant URLSafety
  participant WebhookReceiver
  participant DeliveryLog
  WebhookManager->>Deliverer: provide registered webhook
  Deliverer->>URLSafety: revalidate URL before attempt
  URLSafety-->>Deliverer: safe URL or refusal error
  Deliverer->>WebhookReceiver: send webhook request
  WebhookReceiver-->>Deliverer: HTTP response
  Deliverer->>DeliveryLog: record delivered, failed, or dead status
Loading

Suggested labels: docs, tests

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding SSRF protection for Node webhook delivery.
Linked Issues check ✅ Passed The PR implements the requested SSRF guard on registration and delivery, including URL allowlists, DNS re-resolution, and private-range rejection.
Out of Scope Changes check ✅ Passed The changes stay focused on webhook SSRF protection, with matching code, tests, exports, and documentation updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/node-webhook-ssrf-guard

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

@pratyush618
pratyush618 force-pushed the fix/node-webhook-ssrf-guard branch from 6e57eaf to 695cf57 Compare July 20, 2026 20:17

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/content/docs/node/guides/extensibility/webhooks.mdx (1)

69-69: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document errors for refused deliveries.

Blocked URLs and redirects produce status: "failed" with an error message, not only "dead" deliveries.

Proposed fix
-| `error` | The last error message, if `status` is `"dead"`. |
+| `error` | The last error message, if `status` is not `"delivered"`. |
🤖 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/extensibility/webhooks.mdx` at line 69, Update
the webhook delivery table’s `error` field description to state that it contains
the last error message for failed deliveries, including refused blocked URLs and
redirects, as well as dead deliveries; do not limit the description to status
"dead".
🧹 Nitpick comments (1)
sdks/node/test/core/webhooks.test.ts (1)

10-12: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Restore the process-wide SSRF bypass after these suites. Both assignments persist for the worker process and can mask default-guard behavior in later tests when workers are reused or isolation is disabled.

  • sdks/node/test/core/webhooks.test.ts#L10-L12: snapshot TASKITO_WEBHOOKS_ALLOW_PRIVATE and restore/delete it in suite teardown.
  • sdks/node/test/dashboard/webhookDeliveries.test.ts#L13-L15: apply the same scoped setup and teardown.
🤖 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/test/core/webhooks.test.ts` around lines 10 - 12, Scope the
TASKITO_WEBHOOKS_ALLOW_PRIVATE override to each test suite by snapshotting its
initial value during setup and restoring it, or deleting it when originally
unset, during teardown. Apply this in sdks/node/test/core/webhooks.test.ts lines
10-12 and sdks/node/test/dashboard/webhookDeliveries.test.ts lines 13-15; both
sites require the same setup and teardown behavior.
🤖 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 `@docs/content/docs/node/guides/extensibility/webhooks.mdx`:
- Around line 41-45: Update the webhook SSRF guard description to distinguish
registration-time synchronous hostname/literal-IP checks from delivery-time DNS
validation. State that named hosts resolving to loopback, private, link-local,
or cloud-metadata addresses are rejected before delivery, while preserving the
existing redirect and development override details.

In `@sdks/node/src/webhooks/urlSafety.ts`:
- Around line 104-112: Update the webhook URL safety flow around lookup and
assertAddressAllowed in urlSafety.ts to return the vetted address and address
family instead of discarding DNS results, then update deliverer.ts at the fetch
call to use a custom dispatcher/agent that connects to that approved address
while preserving the original hostname for Host and TLS SNI. Ensure every
resolved address remains validated before selecting the pinned endpoint.

---

Outside diff comments:
In `@docs/content/docs/node/guides/extensibility/webhooks.mdx`:
- Line 69: Update the webhook delivery table’s `error` field description to
state that it contains the last error message for failed deliveries, including
refused blocked URLs and redirects, as well as dead deliveries; do not limit the
description to status "dead".

---

Nitpick comments:
In `@sdks/node/test/core/webhooks.test.ts`:
- Around line 10-12: Scope the TASKITO_WEBHOOKS_ALLOW_PRIVATE override to each
test suite by snapshotting its initial value during setup and restoring it, or
deleting it when originally unset, during teardown. Apply this in
sdks/node/test/core/webhooks.test.ts lines 10-12 and
sdks/node/test/dashboard/webhookDeliveries.test.ts lines 13-15; both sites
require the same setup and teardown behavior.
🪄 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: 049faef6-f2e4-462c-b04d-ae691c1bccb2

📥 Commits

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

📒 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 docs/content/docs/node/guides/extensibility/webhooks.mdx
Comment on lines +104 to +112
let resolved: Array<{ address: string }>;
try {
resolved = await lookup(hostname, { all: true, verbatim: true });
} catch {
throw new UnsafeWebhookUrlError(`could not resolve webhook host ${hostname}`);
}
for (const { address } of resolved) {
assertAddressAllowed(address, hostname);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File map ==\n'
git ls-files 'sdks/node/src/webhooks/*' 'sdks/node/test/**/*webhook*' 'sdks/node/test/**/*webhooks*'

printf '\n== Outline urlSafety.ts ==\n'
ast-grep outline sdks/node/src/webhooks/urlSafety.ts --view expanded || true

printf '\n== Outline deliverer.ts ==\n'
ast-grep outline sdks/node/src/webhooks/deliverer.ts --view expanded || true

printf '\n== Relevant lines urlSafety.ts ==\n'
sed -n '1,220p' sdks/node/src/webhooks/urlSafety.ts | cat -n

printf '\n== Relevant lines deliverer.ts ==\n'
sed -n '1,220p' sdks/node/src/webhooks/deliverer.ts | cat -n

printf '\n== Search for URL safety helpers/usages ==\n'
rg -n "assertAddressAllowed|lookup\\(|UnsafeWebhookUrlError|fetch\\(webhook.url|redirect: \"manual\"|dispatcher|agent|undici|connect" sdks/node/src/webhooks sdks/node/test || true

Repository: ByteVeda/taskito

Length of output: 21586


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,220p' sdks/node/src/webhooks/urlSafety.ts | cat -n
printf '\n---\n'
sed -n '1,220p' sdks/node/src/webhooks/deliverer.ts | cat -n

Repository: ByteVeda/taskito

Length of output: 15353


🏁 Script executed:

#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
for p in [Path('sdks/node/src/webhooks/urlSafety.ts'), Path('sdks/node/src/webhooks/deliverer.ts')]:
    print(f'===== {p} =====')
    text = p.read_text()
    for i, line in enumerate(text.splitlines(), 1):
        if 1 <= i <= 220:
            print(f'{i:4d}: {line}')
PY

Repository: ByteVeda/taskito

Length of output: 15073


Bind webhook delivery to the vetted DNS answer. lookup() checks the host, then discards the result; fetch(webhook.url) can resolve it again and be steered to a different IP on connect. Return the approved address/family and pin the request to it with a custom dispatcher/agent while preserving Host/SNI.

📍 Affects 2 files
  • sdks/node/src/webhooks/urlSafety.ts#L104-L112 (this comment)
  • sdks/node/src/webhooks/deliverer.ts#L58-L65
🤖 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 - 112, Update the
webhook URL safety flow around lookup and assertAddressAllowed in urlSafety.ts
to return the vetted address and address family instead of discarding DNS
results, then update deliverer.ts at the fetch call to use a custom
dispatcher/agent that connects to that approved address while preserving the
original hostname for Host and TLS SNI. Ensure every resolved address remains
validated before selecting the pinned endpoint.

@pratyush618
pratyush618 force-pushed the fix/node-webhook-ssrf-guard branch from 695cf57 to 8a35aab Compare July 20, 2026 20:33
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.
@pratyush618
pratyush618 force-pushed the fix/node-webhook-ssrf-guard branch from 8a35aab to d76396b Compare July 20, 2026 20:40
@stromanni

Copy link
Copy Markdown
Contributor

Superseded by #491, same commits from the fork.

1 similar comment
@pratyush618

Copy link
Copy Markdown
Collaborator Author

Superseded by #491, same commits from the fork.

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

2 participants