Skip to content

fix(api): improve ICS URL validation edge cases#29412

Open
abhinavlevi wants to merge 2 commits into
calcom:mainfrom
abhinavlevi:fix/ics-validation-clean-v5
Open

fix(api): improve ICS URL validation edge cases#29412
abhinavlevi wants to merge 2 commits into
calcom:mainfrom
abhinavlevi:fix/ics-validation-clean-v5

Conversation

@abhinavlevi
Copy link
Copy Markdown

Summary

Fixes ICS feed validation rejecting standards-compliant calendar feeds when URLs do not end with .ics.

The previous validation relied on URL suffix matching instead of validating the actual calendar feed endpoint structure.

This PR:

  • removes brittle .ics suffix enforcement
  • introduces reusable IsCalendarUrl validator
  • validates proper HTTP/HTTPS URLs
  • blocks localhost/loopback hosts
  • supports RFC 5545 / RFC 4791 compliant CalDAV export endpoints
  • adds tests for valid non-.ics calendar feed URLs

Example Previously Rejected

https://caldav.soverin.net/calendars/MyCalendar?export

This endpoint returns:

  • HTTP 200
  • Content-Type: text/calendar
  • valid BEGIN:VCALENDAR

but was rejected only because the URL did not end with .ics.

Why

RFC 5545 and RFC 4791 do not require calendar feed URLs to end with .ics.

Validation should focus on:

  • valid public URL structure
  • allowed protocols
  • reachable calendar feed behavior

rather than filename suffixes.

@github-actions
Copy link
Copy Markdown
Contributor

Welcome to Cal.diy, @abhinavlevi! Thanks for opening this pull request.

A few things to keep in mind:

  • This is Cal.diy, not Cal.com. Cal.diy is a community-driven, fully open-source fork of Cal.com licensed under MIT. Your changes here will be part of Cal.diy — they will not be deployed to the Cal.com production app.
  • Please review our Contributing Guidelines if you haven't already.
  • Make sure your PR title follows the Conventional Commits format.

A maintainer will review your PR soon. Thanks for contributing!

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This pull request adds SSRF-protected calendar feed URL validation. A new IsCalendarUrl custom validator constraint is introduced to validate HTTP/HTTPS URLs with protocol requirements, TLD enforcement (in production), and a blacklist of localhost and loopback hosts. The existing IsICSUrlConstraint is refactored to use this new validation approach instead of checking for .ics file extensions. Test coverage verifies that valid public URLs are accepted, localhost addresses are blocked, and malformed inputs are rejected.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Title check ✅ Passed The title accurately describes the main change: improving ICS URL validation to handle edge cases where URLs lack the .ics suffix, which aligns with the primary objective of the PR.
Description check ✅ Passed The description is directly related to the changeset, providing clear context about RFC compliance, the specific example URL that was previously rejected, and the rationale for the validation changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
apps/api/v2/src/platform/calendars/input/create-ics.input.ts (1)

20-28: ⚡ Quick win

Avoid duplicating URL-security logic in IsICSUrlConstraint.

Line 20-28 duplicates the new calendar URL rules instead of reusing the shared validator. This creates drift risk for future security fixes. Prefer composing with IsCalendarUrlConstraint/IsCalendarUrl and keep one source of truth.

Refactor sketch
-import {
-  ...
-  Validate,
-  ValidatorConstraint,
-  ValidatorConstraintInterface,
-  isURL,
-  ...
-} from "class-validator";
+import { ..., Validate, ValidatorConstraint, ValidatorConstraintInterface, ... } from "class-validator";
+import { IsCalendarUrlConstraint } from "../validators/is-calendar-url";
...
   validate(url: unknown) {
-    if (typeof url !== "string") return false;
-    return isURL(url, {
-      protocols: ["http", "https"],
-      require_protocol: true,
-      require_valid_protocol: true,
-      require_tld: process.env.NODE_ENV === "production",
-      host_blacklist: ["localhost", "127.0.0.1", "::1"],
-    });
+    return new IsCalendarUrlConstraint().validate(url);
   }

Also applies to: 50-50

🤖 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 `@apps/api/v2/src/platform/calendars/input/create-ics.input.ts` around lines 20
- 28, The IsICSUrlConstraint duplicates the calendar URL validation rules;
replace the inline isURL(...) logic with a call or composition that reuses the
existing IsCalendarUrlConstraint / IsCalendarUrl validator to ensure one source
of truth. Specifically, import IsCalendarUrlConstraint or IsCalendarUrl and
either delegate validation inside IsICSUrlConstraint to that constraint's
validate method or apply the IsCalendarUrl decorator where IsICSUrl is used,
removing the duplicated protocols/require_tld/host_blacklist options so future
security fixes only need to be made in IsCalendarUrlConstraint.
apps/api/v2/src/platform/calendars/validators/is-calendar-url.ts (1)

1-7: ⚡ Quick win

Use import type for type-only class-validator imports.

ValidationOptions and ValidatorConstraintInterface are type-only and should be imported with import type to match project standards.

As per coding guidelines, "Use import type { X } for TypeScript type imports".

🤖 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 `@apps/api/v2/src/platform/calendars/validators/is-calendar-url.ts` around
lines 1 - 7, The import statement currently brings in both runtime and type-only
symbols; change it so type-only identifiers are imported with "import type" —
specifically import ValidationOptions and ValidatorConstraintInterface using
"import type { ValidationOptions, ValidatorConstraintInterface }" and keep
registerDecorator, ValidatorConstraint, and isURL as regular imports; update the
import lines near the top of is-calendar-url.ts to reflect this split so
type-only symbols follow project standards.
apps/api/v2/src/platform/calendars/input/create-ics.input.test.ts (1)

28-33: ⚡ Quick win

Add SSRF edge-case tests for numeric/private hosts.

Current security test only checks localhost. Add cases for 127.0.0.1, [::1], and at least one private IPv4 host (e.g., 192.168.1.10) so regressions in host filtering are caught early.

🤖 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 `@apps/api/v2/src/platform/calendars/input/create-ics.input.test.ts` around
lines 28 - 33, Extend the existing SSRF test in
apps/api/v2/src/platform/calendars/input/create-ics.input.test.ts that uses
CreateIcsFeedInputDto and validate: add additional it() cases (or expand the
current case) to assert validation rejects URLs with numeric/private hosts —
include "http://127.0.0.1/fake-calendar", "http://[::1]/fake-calendar" and a
private IPv4 like "http://192.168.1.10/fake-calendar" — each should construct a
CreateIcsFeedInputDto, assign input.urls, call await validate(input), and expect
errors.length to be > 0 so the host filtering covers these SSRF edge cases.
🤖 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 `@apps/api/v2/src/platform/calendars/validators/is-calendar-url.ts`:
- Around line 15-21: Change the two imports ValidationOptions and
ValidatorConstraintInterface to type-only imports (import type {
ValidationOptions, ValidatorConstraintInterface }) so they are erased at
runtime, and replace the brittle host_blacklist-based SSRF check around the
isURL(...) call with an explicit private/loopback IP check: after parsing the
URL/hostname (used with isURL) resolve or validate the hostname to an IP and
reject if net.isIP indicates an address in 127.0.0.0/8, 10.0.0.0/8,
172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, IPv6 ::1, fc00::/7 or fe80::/10;
keep hostname blacklist for literal names like localhost/::1 but rely on the
IP-range check to block other loopback/private addresses before accepting the
URL.

---

Nitpick comments:
In `@apps/api/v2/src/platform/calendars/input/create-ics.input.test.ts`:
- Around line 28-33: Extend the existing SSRF test in
apps/api/v2/src/platform/calendars/input/create-ics.input.test.ts that uses
CreateIcsFeedInputDto and validate: add additional it() cases (or expand the
current case) to assert validation rejects URLs with numeric/private hosts —
include "http://127.0.0.1/fake-calendar", "http://[::1]/fake-calendar" and a
private IPv4 like "http://192.168.1.10/fake-calendar" — each should construct a
CreateIcsFeedInputDto, assign input.urls, call await validate(input), and expect
errors.length to be > 0 so the host filtering covers these SSRF edge cases.

In `@apps/api/v2/src/platform/calendars/input/create-ics.input.ts`:
- Around line 20-28: The IsICSUrlConstraint duplicates the calendar URL
validation rules; replace the inline isURL(...) logic with a call or composition
that reuses the existing IsCalendarUrlConstraint / IsCalendarUrl validator to
ensure one source of truth. Specifically, import IsCalendarUrlConstraint or
IsCalendarUrl and either delegate validation inside IsICSUrlConstraint to that
constraint's validate method or apply the IsCalendarUrl decorator where IsICSUrl
is used, removing the duplicated protocols/require_tld/host_blacklist options so
future security fixes only need to be made in IsCalendarUrlConstraint.

In `@apps/api/v2/src/platform/calendars/validators/is-calendar-url.ts`:
- Around line 1-7: The import statement currently brings in both runtime and
type-only symbols; change it so type-only identifiers are imported with "import
type" — specifically import ValidationOptions and ValidatorConstraintInterface
using "import type { ValidationOptions, ValidatorConstraintInterface }" and keep
registerDecorator, ValidatorConstraint, and isURL as regular imports; update the
import lines near the top of is-calendar-url.ts to reflect this split so
type-only symbols follow project standards.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d5fc65de-c628-4fa5-bc19-42151bcff88d

📥 Commits

Reviewing files that changed from the base of the PR and between 180ede2 and 942b014.

📒 Files selected for processing (3)
  • apps/api/v2/src/platform/calendars/input/create-ics.input.test.ts
  • apps/api/v2/src/platform/calendars/input/create-ics.input.ts
  • apps/api/v2/src/platform/calendars/validators/is-calendar-url.ts

Comment thread apps/api/v2/src/platform/calendars/validators/is-calendar-url.ts Outdated
@abhinavlevi abhinavlevi force-pushed the fix/ics-validation-clean-v5 branch from 942b014 to f9660ca Compare May 19, 2026 10:19
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/api/v2/src/platform/calendars/validators/is-calendar-url.ts (1)

62-62: 💤 Low value

Remove developer debug comment.

The comment "THIS fixes your failing test" is test-specific and not appropriate for production code. Either remove it or replace with a meaningful explanation of the SSRF protection purpose.

-    // DNS SSRF check (THIS fixes your failing test)
+    // Resolve hostname and block if any IP is private/internal (SSRF protection)
🤖 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 `@apps/api/v2/src/platform/calendars/validators/is-calendar-url.ts` at line 62,
Remove the ephemeral debug text "THIS fixes your failing test" from the inline
comment next to the "DNS SSRF check" and either delete the comment or replace it
with a concise, production-ready note explaining the SSRF protection (e.g., why
DNS validation is performed and what threat it mitigates) in the isCalendarUrl
validator.
🤖 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 `@apps/api/v2/src/platform/calendars/validators/is-calendar-url.ts`:
- Line 19: The IPv6 regex in is-calendar-url.ts currently uses an alternation
that only matches exact strings (so prefixes like "fe80::1" or "fd00::1" slip
through); update the regex used in that validator to match IPv6
private/link-local prefixes rather than exact full-string values — specifically
change the pattern referenced in is-calendar-url.ts so it anchors the start (^)
and checks for prefixes like "::1", "fe80:", and the unique-local ranges
starting with "fc" or "fd" (matching the prefix rather than requiring an exact
match) and ensure case-insensitive flag remains; replace the existing
/^(::1|fe80:|fc|fd|::)$/i pattern with a prefix-aware alternative and run tests
for addresses like fe80::1 and fd00::1 to confirm they are blocked.

---

Nitpick comments:
In `@apps/api/v2/src/platform/calendars/validators/is-calendar-url.ts`:
- Line 62: Remove the ephemeral debug text "THIS fixes your failing test" from
the inline comment next to the "DNS SSRF check" and either delete the comment or
replace it with a concise, production-ready note explaining the SSRF protection
(e.g., why DNS validation is performed and what threat it mitigates) in the
isCalendarUrl validator.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 520efda8-ad3e-4ddf-94a6-44148175a0e4

📥 Commits

Reviewing files that changed from the base of the PR and between 942b014 and f9660ca.

📒 Files selected for processing (3)
  • apps/api/v2/src/platform/calendars/input/create-ics.input.test.ts
  • apps/api/v2/src/platform/calendars/input/create-ics.input.ts
  • apps/api/v2/src/platform/calendars/validators/is-calendar-url.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/api/v2/src/platform/calendars/input/create-ics.input.test.ts
  • apps/api/v2/src/platform/calendars/input/create-ics.input.ts

/^192\.168\./.test(ip) ||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(ip) ||
/^169\.254\./.test(ip) ||
/^(::1|fe80:|fc|fd|::)$/i.test(ip)
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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: IPv6 regex does not block private/link-local addresses.

The regex /^(::1|fe80:|fc|fd|::)$/i requires an exact match due to the $ anchor being outside the alternation group. Real IPv6 addresses like fe80::1 (link-local) or fd00::1 (unique-local) will not match, allowing SSRF bypass via IPv6 private addresses.

  • fe80::1234:5678.match(regex) → null (should block, doesn't)
  • fd12:3456::1.match(regex) → null (should block, doesn't)
Proposed fix
   private isPrivateIp(ip: string): boolean {
+    const normalizedIp = ip.toLowerCase();
     return (
       /^127\./.test(ip) ||
       /^10\./.test(ip) ||
       /^192\.168\./.test(ip) ||
       /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(ip) ||
       /^169\.254\./.test(ip) ||
-      /^(::1|fe80:|fc|fd|::)$/i.test(ip)
+      normalizedIp === "::1" ||
+      normalizedIp === "::" ||
+      /^fe80:/i.test(ip) ||
+      /^f[cd]/i.test(ip)
     );
   }
🤖 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 `@apps/api/v2/src/platform/calendars/validators/is-calendar-url.ts` at line 19,
The IPv6 regex in is-calendar-url.ts currently uses an alternation that only
matches exact strings (so prefixes like "fe80::1" or "fd00::1" slip through);
update the regex used in that validator to match IPv6 private/link-local
prefixes rather than exact full-string values — specifically change the pattern
referenced in is-calendar-url.ts so it anchors the start (^) and checks for
prefixes like "::1", "fe80:", and the unique-local ranges starting with "fc" or
"fd" (matching the prefix rather than requiring an exact match) and ensure
case-insensitive flag remains; replace the existing /^(::1|fe80:|fc|fd|::)$/i
pattern with a prefix-aware alternative and run tests for addresses like fe80::1
and fd00::1 to confirm they are blocked.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 21, 2026

CLA assistant check
All committers have signed the CLA.

@dennisameling
Copy link
Copy Markdown

This seems to be a duplicate of #29307

@abhinavlevi
Copy link
Copy Markdown
Author

This seems to be a duplicate of #29307

Thanks for pointing that out.

While both PRs address the same user-facing issue, this implementation takes a broader validation-focused approach instead of only removing the .ics suffix check.

In addition to accepting RFC-compliant non-.ics calendar feed URLs, this PR also:

  • introduces a reusable IsCalendarUrl validator
  • centralizes calendar URL validation logic
  • validates HTTP/HTTPS URL structure
  • blocks localhost/loopback hosts
  • expands coverage for additional edge cases

So the overlap is mainly in the symptom being fixed, while the implementation scope and validation behavior differ.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

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 Jun 3, 2026
@github-actions github-actions Bot removed the Stale label Jun 4, 2026
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.

3 participants