Skip to content

fix: Improve and standardize webhook URL validation#2672

Merged
kodiakhq[bot] merged 3 commits into
mainfrom
drew/improve-webhook-url-validation
Jul 20, 2026
Merged

fix: Improve and standardize webhook URL validation#2672
kodiakhq[bot] merged 3 commits into
mainfrom
drew/improve-webhook-url-validation

Conversation

@pulpdrew

@pulpdrew pulpdrew commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR:

  1. Extracts a shared function for validating webhook URLs (schemes, hostnames, SSRF Private IP protection) and shares it across the internal API (create and update webhook), external API (create and update webhook), webhook /test, and webhook send
  2. Disallows webhook URLs with localhost and *.localhost

This is part of an effort to defend against SSRF. This does not protect against DNS rebinding nor exhaustively enumerate every possible bad hostname.

Screenshots or video

Submitting an invalid webhook:

Screenshot 2026-07-17 at 2 39 12 PM

How to test on Vercel preview

This can be tested locally by creating and updating webhooks via the UI or API, and by executing alerts against those webhooks.

References

  • Linear Issue: Closes HDX-4174
  • Related PRs:

@changeset-bot

changeset-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e6de996

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/api Patch
@hyperdx/app Patch
@hyperdx/otel-collector Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment Jul 20, 2026 10:47am
hyperdx-storybook Ready Ready Preview, Comment Jul 20, 2026 10:47am

Request Review

@github-actions github-actions Bot added the review/tier-4 Critical — deep review + domain expert sign-off label Jul 17, 2026
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🔴 Tier 4 — Critical

Touches auth, data models, config, tasks, OTel pipeline, ClickHouse, or CI/CD.

Why this tier:

  • Critical-path files (3):
    • packages/api/src/config.ts
    • packages/api/src/routers/external-api/v2/webhooks.ts
    • packages/api/src/tasks/checkAlerts/template.ts

Review process: Deep review from a domain expert. Synchronous walkthrough may be required.
SLA: Schedule synchronous review within 2 business days.

Stats
  • Production files changed: 7
  • Production lines changed: 248 (+ 307 in test files, excluded from tier calculation)
  • Branch: drew/improve-webhook-url-validation
  • Author: pulpdrew

To override this classification, remove the review/tier-4 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR standardizes webhook URL validation for webhook writes, tests, and alert delivery. The main changes are:

  • A shared webhook URL validator for internal, external, test, and send paths.
  • Blocking for localhost, localhost subdomains, private IPs, and reserved IPs.
  • An optional webhook hostname allowlist in API configuration and Docker setup.
  • Unit and integration coverage for create, update, test, and delivery validation.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
packages/api/src/utils/validators.ts Adds the shared webhook URL validator, hostname normalization, private address checks, configured service host blocking, and allowlist handling.
packages/api/src/routers/api/webhooks.ts Routes internal create, update, and test requests through the shared validator before persistence or outbound test delivery.
packages/api/src/routers/external-api/v2/webhooks.ts Routes external API create and update requests through the shared validator before writing webhook records.
packages/api/src/tasks/checkAlerts/template.ts Reuses the shared validator before alert webhook delivery and logs blocked delivery attempts with webhook context.

Reviews (9): Last reviewed commit: "Merge branch 'main' into drew/improve-we..." | Re-trigger Greptile

Comment thread packages/api/src/utils/validators.ts Outdated
Comment thread packages/api/src/utils/validators.ts Outdated
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 241 passed • 1 skipped • 1009s

Status Count
✅ Passed 241
❌ Failed 0
⚠️ Flaky 1
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found. The extraction is sound: validateWebhookUrl runs before every outbound path (internal create/update/test, external v2 create/update, and alert send), errors are mapped to 400 in the routers and swallowed-then-logged per-channel in the alert task (confirmed one blocked webhook does not abort sibling channels — checkAlerts/index.ts:1096 catches per-fire and pushes to executionErrors). The WebhookService enum is the same symbol in the model and validator (both import from @hyperdx/common-utils/dist/types), so the assertion signature is compatible. No P0/P1 issues were substantiated.

🟡 P2 — recommended

  • packages/api/src/utils/validators.ts:155 — Moving enforcement to create/update time turns localhost/*.localhost and private-IP webhooks into a hard 400, so self-hosted deployments that legitimately target loopback/internal hosts break by default and pre-existing such webhooks can no longer be saved — even a name-only edit hits validateWebhookUrl(resolvedUrl) at routers/api/webhooks.ts:286 and is rejected.
    • Fix: Call out the WEBHOOK_HOSTNAME_ALLOWLIST migration path in the release notes, and skip revalidation on update when the resolved URL is unchanged so existing webhooks remain editable while the send path still enforces the block.
🔵 P3 nitpicks (5)
  • packages/api/src/routers/external-api/v2/webhooks.ts:30handleWebhookUrlValidationError is duplicated verbatim in both routers/api/webhooks.ts:116 and this file, against the PR's stated goal of centralizing webhook validation logic.
    • Fix: Move the helper next to validateWebhookUrl in utils/validators.ts and import it in both routers.
    • maintainability
  • packages/api/src/utils/validators.ts:70 — The module-level WEBHOOK_HOSTNAME_ALLOWLIST (a normalized string[]) shadows the identically named config.WEBHOOK_HOSTNAME_ALLOWLIST (the raw comma-separated string), which is easy to misread on a future edit.
    • Fix: Rename the derived local to something distinct, e.g. PARSED_WEBHOOK_HOSTNAME_ALLOWLIST.
    • maintainability
  • packages/api/src/utils/validators.ts:146 — The blocked-host and private-IP error messages no longer include the offending hostname, so operators cannot tell which host tripped the block from the response or logs alone.
    • Fix: Interpolate the normalized hostname (not the full URL, to avoid leaking credentials/query params) into the WebhookUrlValidationError message.
    • reliability
  • packages/api/src/tasks/checkAlerts/template.ts:66logBlockedWebhookDelivery logs only id and team, dropping the webhook name/URL the old inline validator logged, reducing on-call debuggability.
    • Fix: Add webhook.name and a redacted/truncated webhook.url to the log payload.
    • reliability
  • packages/api/src/utils/validators.ts:87 — Computing WEBHOOK_HOSTNAME_ALLOWLIST and BLOCKED_WEBHOOK_HOSTS at module load forces the test to use jest.isolateModules/doMock and means config changes require a process restart.
    • Fix: Optionally compute (or memoize) these lazily inside validateWebhookUrl; this mirrors the pre-existing pattern, so treat as advisory.
    • maintainability

Reviewers (3 returned): maintainability, reliability, learnings-researcher. Correctness, security, adversarial, api-contract, testing, kieran-typescript, project-standards, and agent-native reviewers were dispatched but had not returned when output was required; the orchestrator independently verified those dimensions against the diff, both router schemas, the alert-task error path, and the test files.

Testing gaps:

  • No test covers the update-time lockout — editing a webhook whose stored URL is now disallowed (localhost/private IP) returning 400.
  • No test asserts a blocked delivery in the alert task (WebhookUrlValidationError) is logged/counted once and does not abort delivery to sibling channels.
  • SSRF scope note (per author intent): isPrivateIp only inspects IP literals, so a public hostname that resolves to a private address (DNS rebinding/resolution) and post-validation redirects to internal hosts remain out of scope and unguarded.

Comment thread packages/api/src/utils/validators.ts
@pulpdrew
pulpdrew requested review from a team and jordan-simonovski and removed request for a team and bot-hyperdx July 17, 2026 20:14
@teeohhem

Copy link
Copy Markdown
Contributor

@pulpdrew

packages/api/src/utils/validators.ts:57 -- Non-canonical numeric host encodings (decimal http://2130706433, hex http://0x7f000001, short-dotted http://127.1) are reported as non-private by isPrivateIp and pass validation, yet resolve to loopback/private ranges when the webhook is actually fetched, leaving an SSRF path open.

Wonder how strict we want to be here.

packages/api/src/tasks/checkAlerts/template.ts:294 -- Tightened rules now throw at delivery for webhooks stored before this change, so a non-dev deployment with an existing localhost/*.localhost generic webhook will silently stop delivering alerts.

Not sure of the broad use case. But what do you think about an env var override/check for this? Something like WEBHOOK_DOMAIN_LOCAL_ALLOWLIST ?

@pulpdrew

Copy link
Copy Markdown
Contributor Author

packages/api/src/utils/validators.ts:57 -- Non-canonical numeric host encodings (decimal http://2130706433, hex http://0x7f000001, short-dotted http://127.1) are reported as non-private by isPrivateIp and pass validation, yet resolve to loopback/private ranges when the webhook is actually fetched, leaving an SSRF path open.

This is not true, since we pass new URL(...).hostname to isPrivateIp, and URL.hostname already normalizes each of those to 127.0.0.1.

Screenshot 2026-07-17 at 4 27 48 PM

@pulpdrew

Copy link
Copy Markdown
Contributor Author

But what do you think about an env var override/check for this? Something like WEBHOOK_DOMAIN_LOCAL_ALLOWLIST?

Great idea!

@jordan-simonovski jordan-simonovski left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No notes. LGTM.

@kodiakhq
kodiakhq Bot merged commit 01508d1 into main Jul 20, 2026
27 checks passed
@kodiakhq
kodiakhq Bot deleted the drew/improve-webhook-url-validation branch July 20, 2026 10:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge review/tier-4 Critical — deep review + domain expert sign-off

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants