calcom/cal.com
Security
The trust boundaries Cal.diy crosses and how the codebase defends each one.
Threat model
Cal.diy runs as a multi-tenant scheduling platform. The major trust boundaries:
- Public booking page — anonymous internet visitors hit
/[user]/[eventType]. Untrusted input flows into slot queries and booking submissions. - Authenticated dashboard — logged-in users (or team members) see and edit their own data.
- Admin / system actions — promoted users with cross-tenant power.
- Inbound webhooks — third-party services (Stripe, Daily, calendar providers) deliver signed payloads.
- Outbound calls — calendar / video / payment / CRM APIs receive credentials and accept event payloads.
- API v2 surface — third-party developers and Platform clients call the REST API.
- Embeds — Cal.diy is loaded inside iframes on third-party sites; postMessage is the only sanctioned channel.
Encryption at rest
The headline secret is CALENDSO_ENCRYPTION_KEY — a 24-byte key (openssl rand -base64 24) used by packages/lib/crypto.ts (AES-256-CBC) to encrypt:
Credential.key(every third-party access/refresh token)- 2FA secrets on
User - Some app-specific secrets
AGENTS.md is explicit: Credential.key must never be selected into anything that flows to a client. Repositories use select to omit it; audit logs and Sentry payloads run through packages/lib/redactSensitiveData.ts.
NEXTAUTH_SECRET (32-byte base64) signs NextAuth's JWT and CSRF tokens.
SSRF protection
Cal.diy makes outbound HTTP calls in many places (calendar providers, webhooks, redirect helpers). packages/lib/ssrfProtection.ts blocks SSRF attempts:
- Rejects private IPv4 ranges (10/8, 172.16/12, 192.168/16, 169.254/16)
- Rejects loopback (127/8, ::1)
- Rejects link-local and unique local addresses
- Resolves DNS and rechecks
- Used by the fetch wrapper in
packages/lib/fetch-wrapper.ts
Input validation
- Every tRPC procedure has a Zod schema (
<action>.schema.ts). The router refuses requests that don't validate. - API v2 controllers use
class-validatorDTOs. - Booking responses are validated through a per-event-type schema generated from the booking-fields configuration (
packages/features/bookings/lib/getBookingResponsesSchema.ts). - Markdown content is sanitized through
packages/lib/markdownToSafeHTML.ts(markdown-it + DOMPurify).
Redirect safety
packages/lib/getSafeRedirectUrl.ts validates a redirect URL against the configured allowlist. Used after authentication and after the booking flow.
Rate limiting
- Web app —
packages/lib/rateLimit.ts(Redis token bucket) is wrapped around tRPC procedures and Pages-Router APIs. - API v2 —
CustomThrottlerGuard(apps/api/v2/src/lib/throttler-guard.ts), Redis-backed, registered asAPP_GUARD. - IP extraction — both rely on
packages/lib/getIP.ts, which is X-Forwarded-For aware.
Webhooks (inbound)
Inbound webhooks (Stripe, Daily, Vercel deployments, calendar push notifications) verify signatures before mutating state:
- Stripe — verified with the Stripe webhook secret in
packages/app-store/stripepayment. - Vercel deployment webhook —
apps/api/v2/src/vercel-webhook.guard.tschecks the configured shared secret. - Calendar push notifications — verified per-provider in each app-store adapter; the body is fetched fresh from the API (we don't trust the push payload).
Webhooks (outbound)
Outbound webhooks (features/webhooks) HMAC-sign each payload with a per-subscriber secret. Subscribers verify the signature before trusting the payload.
Auth hardening
- Passwords hashed with bcrypt; rules in
packages/features/auth/lib/validPassword.ts. - Email verification required for new signups (
sendVerificationRequest.ts). - TOTP-based 2FA (
packages/lib/totp.ts). - Bot detection on signup via
packages/features/bot-detectionandpackages/features/watchlist. - Session cookies follow
packages/lib/default-cookies.ts(SameSite=Lax,Securein production).
Authorization
- PBAC (permission-based access control) governs team / org boundaries. SQL helpers in
packages/prisma/sql/, runtime checks inpackages/features/membershipand per-resource code paths. - Permission checks belong in
page.tsx, neverlayout.tsx(AGENTS.md). - The
Credential.keyrule is enforced as a coding convention, not a DB constraint. Reviewers watch forselect: { key: true }slipping in.
Embed boundary
embed-core only accepts a fixed set of postMessage commands; the iframe-side runtime in apps/web validates event.origin before reacting to messages. The published bundle pins NEXT_PUBLIC_WEBAPP_URL so embeds can't be tricked into pointing at a hostile origin.
Sensitive log redaction
packages/lib/redactSensitiveData.ts and packages/lib/piiFreeData.ts strip:
- Passwords, password resets
- API keys, OAuth tokens
credential.key(the encrypted blob)- Email addresses where the surface doesn't need them
- IPs where the surface doesn't need them
Both tslog configurations apply this redaction by default.
Reporting
SECURITY.md at the repo root describes responsible disclosure. The repo also has a .snyk policy file for the Snyk scanner inside apps/api/v2/.
Where to look
packages/lib— almost every security helper lives herefeatures/auth— auth surfacefeatures/credentials— credential lifecycle and the encryption ruleapps/api/v2— the REST surface and its guards
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.