Cal.com is a widely deployed open-source scheduling platform (Calendly alternative) built on Next.js, used by businesses and individuals to manage meeting bookings, availability, and calendar integrations — it stores attendee data, business meeting schedules, and payment information. Key assessment areas: NEXTAUTH_SECRET in .env is used by NextAuth.js to sign JWT session tokens, and its compromise enables forging sessions as any Cal.com user; GOOGLE_CLIENT_SECRET authenticates the Google OAuth app for calendar and SSO integration; STRIPE_PRIVATE_KEY provides live Stripe API access; DATABASE_URL contains the PostgreSQL connection string; and the Cal.com API allows enumerating all bookings and user accounts. This guide covers systematic Cal.com security assessment.
# Cal.com — authentication and API key testing
CALCOM_URL="https://cal.example.com"
# Cal.com API key — generated in Settings > Security > API Keys
API_KEY="cal_live_your_api_key_here"
# Verify API key and get current user
curl -s "${CALCOM_URL}/api/v1/me" \
-H "Authorization: Bearer ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
if 'user' in d:
u = d['user']
print(f'Authenticated: {u.get(\"name\")} ({u.get(\"email\")})')
print(f'Role: {u.get(\"role\")} id={u.get(\"id\")}')
else:
print(f'Failed: {d}')
" 2>/dev/null
# Test admin-level API keys — look for endpoints that require admin scope
# Cal.com API key scopes vary by key type
curl -s "${CALCOM_URL}/api/v1/users" \
-H "Authorization: Bearer ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
if 'users' in d:
users = d.get('users',[])
print(f'Users: {len(users)}')
for u in users[:5]:
print(f' [{u.get(\"id\")}] {u.get(\"name\")} email={u.get(\"email\")} role={u.get(\"role\")}')
else:
print(f'Response: {str(d)[:200]}')
" 2>/dev/null
# Check for .env exposure via Next.js config endpoints
curl -s "${CALCOM_URL}/api/auth/csrf" 2>/dev/null | python3 -c "
import json,sys
print(json.load(sys.stdin))
" 2>/dev/null
# Cal.com REST API — booking and user enumeration
CALCOM_URL="https://cal.example.com"
API_KEY="cal_live_your_api_key_here"
# List all bookings — all scheduled meetings with attendee data
curl -s "${CALCOM_URL}/api/v1/bookings" \
-H "Authorization: Bearer ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
bookings = d.get('bookings',[])
print(f'Bookings: {len(bookings)}')
for b in bookings[:5]:
attendees = b.get('attendees',[])
print(f' [{b.get(\"id\")}] {b.get(\"title\")} start={str(b.get(\"startTime\",\"\"))[:19]}')
for a in attendees:
print(f' Attendee: {a.get(\"name\")} ({a.get(\"email\")})')
" 2>/dev/null
# List all event types
curl -s "${CALCOM_URL}/api/v1/event-types" \
-H "Authorization: Bearer ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
event_types = d.get('event_types',[])
print(f'Event types: {len(event_types)}')
for e in event_types[:10]:
print(f' [{e.get(\"id\")}] {e.get(\"title\")} length={e.get(\"length\")}min slug={e.get(\"slug\")}')
" 2>/dev/null
# List all schedules (availability)
curl -s "${CALCOM_URL}/api/v1/schedules" \
-H "Authorization: Bearer ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
schedules = d.get('schedules',[])
print(f'Schedules: {len(schedules)}')
for s in schedules[:5]:
print(f' [{s.get(\"id\")}] {s.get(\"name\")} timeZone={s.get(\"timeZone\")} isDefault={s.get(\"isDefault\")}')
" 2>/dev/null
# Cal.com .env credential extraction
# .env file — all application secrets
cat /app/.env 2>/dev/null | grep -E "NEXTAUTH|GOOGLE|STRIPE|DATABASE|SMTP|ZOOM|TWILIO|SECRET"
# NEXTAUTH_SECRET — Next.js/NextAuth.js session JWT signing key
# GOOGLE_CLIENT_SECRET — Google OAuth2 client secret (Calendar + SSO)
# STRIPE_PRIVATE_KEY — Stripe live secret key (sk_live_...)
# DATABASE_URL — PostgreSQL connection string
# EMAIL_SERVER_PASSWORD — SMTP password for transactional email
# ZOOM_CLIENT_SECRET — Zoom OAuth integration secret
# Docker deployment
docker inspect calcom 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
for c in d:
for e in c.get('Config',{}).get('Env',[]):
if any(k in e for k in ['NEXTAUTH','GOOGLE','STRIPE','DATABASE','SECRET','ZOOM']):
print(e)
" 2>/dev/null
# PostgreSQL direct access — users and credentials
DATABASE_URL="postgresql://cal:PASSWORD@localhost/cal"
psql "$DATABASE_URL" 2>/dev/null << 'EOF'
-- All Cal.com users with email and role
SELECT id, name, email, role, bio, "createdDate",
"identityProvider", "twoFactorEnabled"
FROM "User"
ORDER BY role, "createdDate" DESC
LIMIT 20;
EOF
# credential_keys — OAuth tokens for integrations (Google Calendar, Zoom, etc.)
psql "$DATABASE_URL" 2>/dev/null << 'EOF'
SELECT ck.id, ck."userId", u.email, ck.type,
ck.key::text -- Contains access_token, refresh_token, expiry_date
FROM "CredentialKey" ck
JOIN "User" u ON ck."userId" = u.id
ORDER BY u.email
LIMIT 20;
EOF
# key column: JSON with access_token and refresh_token for Google Calendar, Zoom, etc.
# API keys table
psql "$DATABASE_URL" 2>/dev/null << 'EOF'
SELECT ak.id, ak."userId", u.email, ak."hashedKey", ak."note",
ak."createdAt", ak."lastUsedAt", ak."expiresAt"
FROM "ApiKey" ak
JOIN "User" u ON ak."userId" = u.id
ORDER BY ak."lastUsedAt" DESC
LIMIT 20;
EOF
openssl rand -base64 32; restrict .env to 600 owned by the Cal.com process; use Docker secrets or a secrets manager in production; rotate NEXTAUTH_SECRET if exposed (invalidates all existing sessions, requiring all users to re-authenticate)| Security Test | Method | Risk |
|---|---|---|
| NEXTAUTH_SECRET extraction and session token forgery | Read .env NEXTAUTH_SECRET — NextAuth.js JWT signing key; forge valid session cookies as any Cal.com user bypassing OAuth authentication entirely | Critical |
| API booking and attendee enumeration | GET /api/v1/bookings — all scheduled meetings with attendee names and emails; complete view of all business meetings with external parties | High |
| CredentialKey table OAuth token extraction | PostgreSQL SELECT from CredentialKey — access_token and refresh_token for Google Calendar, Zoom, etc.; enables reading/writing user calendars and creating unauthorized Zoom meetings | Critical |
| STRIPE_PRIVATE_KEY extraction and payment API access | Read .env STRIPE_PRIVATE_KEY — Stripe live secret key; direct Stripe API access for listing customers, creating charges, processing refunds, and modifying subscriptions | Critical |
| Google OAuth credential extraction | Read .env GOOGLE_CLIENT_SECRET — OAuth application secret; enables authorization code interception and impersonation of the Cal.com Google OAuth application | Critical |
Ironimo tests Cal.com deployments for NEXTAUTH_SECRET extraction and JWT session token forgery, STRIPE_PRIVATE_KEY live payment API access, Google OAuth credential extraction, CredentialKey table OAuth token extraction (Google Calendar, Zoom), API booking and attendee data enumeration, user email harvesting, API key audit and expiration verification, DATABASE_URL PostgreSQL credential extraction, Docker environment variable credential exposure, and SMTP password extraction for email service impersonation.
Start free scan