Skip to content

fix: add URL validation to webhook endpoints#26593

Merged
pedroccastro merged 21 commits intomainfrom
fix/webhook-url-validation
Feb 5, 2026
Merged

fix: add URL validation to webhook endpoints#26593
pedroccastro merged 21 commits intomainfrom
fix/webhook-url-validation

Conversation

@pedroccastro
Copy link
Copy Markdown
Contributor

@pedroccastro pedroccastro commented Jan 8, 2026

What does this PR do?

Adds URL validation to webhook endpoints to prevent SSRF attacks (malicious URLs pointing to internal networks, cloud
metadata endpoints, and localhost).

Existing webhooks are preserved: validation only applies when the URL is created or changed, allowing existing
HTTP webhooks to continue working.

Note: This PR depends on #26522 and should be merged after it.

Changes

Layer File(s) Change
Lib ssrfProtection.ts SSRF validation with ipaddr.js
tRPC edit.schema.ts Simple URL validation (no SSRF at schema level)
tRPC edit.handler.ts Conditional SSRF validation (only when URL changes)
API v2 validate-webhook-url.ts Helper functions for SSRF validation
API v2 *-webhooks.service.ts Validate on create; validate on update only if URL changes

Validation rules

  • HTTPS only (except self-hosted deployments)
  • Private IP ranges blocked (RFC1918, RFC6598, link-local, etc.)
  • Cloud metadata endpoints blocked (AWS, GCP, Azure)
  • Localhost/loopback blocked (except E2E tests)

Exceptions

Environment HTTP Private IPs Cloud Metadata
Cal.com No No No
Self-hosted Yes Yes No
E2E tests Yes (localhost only) No No

Grandfathering behavior

Action URL changed? Validation
Create webhook N/A Always validated
Update webhook No Skipped
Update webhook Yes Validated

Updates since last revision

  • Protocol restriction for self-hosted mode: Self-hosted deployments now only allow HTTP and HTTPS protocols. Previously, arbitrary protocols (file://, ftp://, etc.) could bypass validation. This addresses Cubic AI review feedback (confidence 9/10).

Breaking Changes Assessment

Existing users:

  • Existing webhooks continue working (validation only on create/URL change)
  • Self-hosted deployments can still use HTTP webhooks
  • E2E tests work with localhost receivers

New restrictions (Cal.com only):

  • New webhooks require HTTPS
  • Cannot change existing webhook URL to HTTP/private IP

How should this be tested?

  1. Create webhook with https://example.com/hook → ✓ works
  2. Create webhook with http://example.com/hook → X rejected
  3. Create webhook with https://localhost/hook → X rejected
  4. Create webhook with https://10.0.0.1/hook → X rejected
  5. Edit existing HTTP webhook (change active only) → ✓ works (grandfathered)
  6. Edit existing HTTP webhook (change URL to another HTTP) → X rejected
  7. Self-hosted deployment → HTTP webhooks allowed
  8. Self-hosted deploymentfile:// or ftp:// URLs rejected

Human Review Checklist

  • Verify protocol check in ssrfProtection.ts:137-139 correctly restricts self-hosted to HTTP/HTTPS only
  • Confirm E2E localhost exception (NEXT_PUBLIC_IS_E2E) cannot be exploited in production
  • Review IPv6 bracket stripping logic for edge cases

Mandatory Tasks

  • I have self-reviewed the code
  • N/A I have updated the developer docs
  • I confirm automated tests are in place

@pedroccastro pedroccastro marked this pull request as ready for review January 8, 2026 19:46
@pedroccastro pedroccastro requested review from a team as code owners January 8, 2026 19:46
@graphite-app graphite-app bot added foundation core area: core, team members only labels Jan 8, 2026
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

4 issues found across 16 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/lib/ssrfProtection.ts">

<violation number="1" location="packages/lib/ssrfProtection.ts:138">
P0: IPv6 addresses bypass private IP validation because `URL.hostname` includes brackets for IPv6 (e.g., `[::1]`), but `net.isIP('[::1]')` returns 0 due to brackets. This allows requests to private IPv6 addresses like `https://[::1]/` (localhost) to pass validation. Strip brackets before checking.</violation>

<violation number="2" location="packages/lib/ssrfProtection.ts:203">
P2: Rule violated: **Avoid Logging Sensitive Information**

URL logged may contain sensitive data (API keys, tokens, or basic auth credentials in query parameters). Consider sanitizing the URL to log only the hostname and path, or redact query parameters before logging.</violation>
</file>

<file name="apps/api/v2/src/modules/teams/teams/inputs/update-team.input.ts">

<violation number="1" location="apps/api/v2/src/modules/teams/teams/inputs/update-team.input.ts:21">
P2: Replacing `@IsUrl()` with `@IsString()` + `@Validate(SSRFSafeUrlValidator)` allows empty strings to pass validation. The `SSRFSafeUrlValidator` uses `if (!url) return true;` which treats empty string as valid (since `!''` is truthy), whereas `@IsUrl()` would reject empty strings. Consider adding `@IsNotEmpty()` decorator or fixing the validator to check `url === undefined || url === null` instead of `!url`.</violation>
</file>

<file name="apps/api/v2/src/modules/webhooks/validators/webhookUrlValidator.ts">

<violation number="1" location="apps/api/v2/src/modules/webhooks/validators/webhookUrlValidator.ts:20">
P1: Security mismatch: Code uses `allowHttp: true` but PR description specifies "HTTPS only" and testing instructions say HTTP should be rejected. If HTTP should be blocked per security requirements, change to `allowHttp: false`. If HTTP is intentionally allowed, update the PR description and testing instructions to reflect the actual behavior.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@pedroccastro pedroccastro force-pushed the fix/webhook-url-validation branch from aa55415 to 148e550 Compare January 8, 2026 20:17
@vercel
Copy link
Copy Markdown

vercel bot commented Jan 8, 2026

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

4 Skipped Deployments
Project Deployment Review Updated (UTC)
api-v2 Ignored Ignored Preview Jan 10, 2026 10:59am
cal Ignored Ignored Jan 10, 2026 10:59am
cal-companion Ignored Ignored Preview Jan 10, 2026 10:59am
cal-eu Ignored Ignored Jan 10, 2026 10:59am

Add URL validation to webhook endpoints to prevent malicious URLs pointing
to internal networks, cloud metadata endpoints, and localhost

- Add URL validation schema for tRPC webhook endpoints
- Add WebhookUrlValidator for API v2 NestJS endpoints
- Apply validation to create, edit, and testTrigger endpoints

Security: Only HTTPS allowed. Blocks private IPs (RFC1918, RFC6598),
localhost, and cloud metadata endpoints (AWS, GCP, Azure)
- Fix IPv6 bypass: strip brackets before IP check (e.g., [::1] -> ::1)
- Sanitize URLs in logs: remove query params that may contain secrets
- Add tests for IPv6 private addresses with brackets
Udit-takkar
Udit-takkar previously approved these changes Jan 12, 2026
@Udit-takkar Udit-takkar enabled auto-merge (squash) January 12, 2026 08:16
hariombalhara
hariombalhara previously approved these changes Jan 12, 2026
Copy link
Copy Markdown
Member

@hariombalhara hariombalhara left a comment

Choose a reason for hiding this comment

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

Small non-blocking suggestions

- Replace manual regex patterns with ipaddr.js library for robust IP validation
- Fix IPv6 bypass vulnerability where bracketed addresses ([::1]) were not blocked
- Add reserved and benchmarking IP ranges to blocklist
- Remove unused allowHttp option (HTTPS-only policy)
- DRY refactor Zod URL schemas
Copy link
Copy Markdown
Contributor

@volnei volnei left a comment

Choose a reason for hiding this comment

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

Small comment. Rest looks good 🚀

@pedroccastro pedroccastro marked this pull request as draft January 13, 2026 03:40
auto-merge was automatically disabled January 13, 2026 03:40

Pull request was converted to draft

@vercel vercel bot temporarily deployed to Preview – cal-companion January 13, 2026 08:55 Inactive
@vercel vercel bot temporarily deployed to Preview – dev January 13, 2026 08:55 Inactive
@github-actions
Copy link
Copy Markdown
Contributor

This PR has been marked as stale due to inactivity. If you're still working on it or need any help, please let us know or update the PR to keep it active.

@github-actions github-actions bot added the Stale label Jan 21, 2026
- tRPC: validate URL change in edit.handler.ts instead of schema
- API v2: validate in services using shared utility functions
- Remove unused WebhookUrlValidator class-validator decorator
- Add DRY helpers: validateWebhookUrl, validateWebhookUrlIfChanged
@github-actions github-actions bot removed the Stale label Jan 22, 2026
@pedroccastro pedroccastro marked this pull request as ready for review January 22, 2026 14:01
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 15 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="apps/api/v2/src/modules/webhooks/services/webhooks.service.ts">

<violation number="1" location="apps/api/v2/src/modules/webhooks/services/webhooks.service.ts:11">
P2: Fetch only `subscriberUrl` instead of loading the entire webhook row. The new call introduces a full-row Prisma read for a single field, which is against the select-only guidance and increases data exposure.</violation>
</file>

<file name="packages/lib/zod/ssrfSafeUrl.ts">

<violation number="1" location="packages/lib/zod/ssrfSafeUrl.ts:36">
P2: The new non-nullable optional schema still accepts an empty string because it reuses validateSsrfUrl. That allows clearing the URL with "" without SSRF validation, which defeats the intent of a non-nullable update field.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@github-actions
Copy link
Copy Markdown
Contributor

Devin AI is addressing Cubic AI's review feedback

A Devin session has been created to address the issues identified by Cubic AI.

View Devin Session

- Use select-only query for subscriberUrl comparison (reduces data exposure)
- Reject empty string in non-nullable URL schema
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 3, 2026

E2E results are ready!

@paragon-review
Copy link
Copy Markdown

paragon-review bot commented Feb 3, 2026

Paragon: tests updated

1 updated test generated for this PR.

Updated Tests

  • HTTP webhook exceptions for self-hosted deployments — Adding new test to existing file

Accept Changes Open in Paragon

Details

Updated Tests

  • HTTP webhook exceptions for self-hosted deployments (unit)

@paragon-review
Copy link
Copy Markdown

paragon-review bot commented Feb 3, 2026

Paragon: tests updated

1 new test and 1 updated test generated for this PR.

New Tests

  • validate-webhook-url.spec.ts - Webhook URL validation utility tests — Tests for the new validate-webhook-url.ts utility that provides validateWebhookUrl and validateWebhookUrlIfChanged functions used by webhook services.

Updated Tests

  • ssrfProtection.test.ts - Self-hosted behavior tests — Adding new test to existing file

Accept Changes Open in Paragon

Details

New Tests

  • validate-webhook-url.spec.ts - Webhook URL validation utility tests (unit)

Updated Tests

  • ssrfProtection.test.ts - Self-hosted behavior tests (unit)

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/lib/ssrfProtection.ts">

<violation number="1" location="packages/lib/ssrfProtection.ts:134">
P1: Self-hosted mode allows arbitrary protocols (file://, ftp://, etc.) instead of restricting to HTTP/HTTPS. According to the PR's validation rules, self-hosted should allow HTTP, but this allows all protocols. Consider adding a protocol check before returning valid.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

…l.spec.ts

Generated by Paragon from proposal for PR #26593
Generated by Paragon from proposal for PR #26593
Copy link
Copy Markdown
Contributor Author

🧪 Tests Added by Paragon

The following test files have been added to this PR:

  • apps/api/v2/src/modules/webhooks/utils/validate-webhook-url.spec.ts
  • packages/lib/ssrfProtection.test.ts

These tests were generated from an approved test proposal.


Generated with Paragon

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 3, 2026

Devin AI is addressing Cubic AI's review feedback

A Devin session has been created to address the issues identified by Cubic AI.

View Devin Session

devin-ai-integration bot and others added 2 commits February 3, 2026 19:39
Address Cubic AI review feedback (confidence 9/10): Self-hosted mode was
allowing arbitrary protocols (file://, ftp://, etc.) instead of restricting
to HTTP/HTTPS. Added protocol check before returning valid for self-hosted
deployments.

Co-Authored-By: unknown <>
Copy link
Copy Markdown
Contributor

@emrysal emrysal left a comment

Choose a reason for hiding this comment

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

Awesome, small NIT; but non-blocking; otherwise approved.

}
}

export function validateWebhookUrlIfChanged(
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.

NIT: I prefer assert naming for throwing functions like this.

@pedroccastro pedroccastro merged commit cb36fc2 into main Feb 5, 2026
89 of 92 checks passed
@pedroccastro pedroccastro deleted the fix/webhook-url-validation branch February 5, 2026 19:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants