From 51e852f671035a5f03aaaa4c4524c6f04bacfe19 Mon Sep 17 00:00:00 2001 From: Pedro Castro Date: Tue, 14 Apr 2026 08:29:10 -0300 Subject: [PATCH 1/5] refactor: narrow user fields returned by dynamic event endpoint (#28875) --- .../features/eventtypes/lib/getPublicEvent.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/features/eventtypes/lib/getPublicEvent.ts b/packages/features/eventtypes/lib/getPublicEvent.ts index 2d106e599a43f0..d587b1f36104f8 100644 --- a/packages/features/eventtypes/lib/getPublicEvent.ts +++ b/packages/features/eventtypes/lib/getPublicEvent.ts @@ -345,16 +345,26 @@ export const getPublicEvent = async ( bookingFields: getBookingFieldsWithSystemFields({ ...defaultEvent, disableBookingTitle }), restrictionScheduleId: null, useBookerTimezone: false, - // Clears meta data since we don't want to send this in the public api. + // Only return fields consumed by the booker. subsetOfUsers: users.map((user) => ({ - ...user, - metadata: undefined, + name: user.name, + username: user.username, + avatarUrl: user.avatarUrl, + weekStart: user.weekStart, + brandColor: user.brandColor, + darkBrandColor: user.darkBrandColor, + profile: user.profile, bookerUrl: getBookerBaseUrlSync(user.profile?.organization?.slug ?? null), })), users: fetchAllUsers ? users.map((user) => ({ - ...user, - metadata: undefined, + name: user.name, + username: user.username, + avatarUrl: user.avatarUrl, + weekStart: user.weekStart, + brandColor: user.brandColor, + darkBrandColor: user.darkBrandColor, + profile: user.profile, bookerUrl: getBookerBaseUrlSync(user.profile?.organization?.slug ?? null), })) : undefined, From cbb234cb35ca3cec3b467150dbf0c41ee48046cc Mon Sep 17 00:00:00 2001 From: Pedro Castro Date: Tue, 14 Apr 2026 08:29:26 -0300 Subject: [PATCH 2/5] fix: validate credential ownership in attribute sync update (#28873) --- .../IIntegrationAttributeSyncRepository.ts | 3 ++- .../IntegrationAttributeSyncService.ts | 7 +++++ .../updateAttributeSync.handler.ts | 27 ++++++++++++++++++- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/features/ee/integration-attribute-sync/repositories/IIntegrationAttributeSyncRepository.ts b/packages/features/ee/integration-attribute-sync/repositories/IIntegrationAttributeSyncRepository.ts index fe571fa50b116a..af0ec8ba02ebd4 100644 --- a/packages/features/ee/integration-attribute-sync/repositories/IIntegrationAttributeSyncRepository.ts +++ b/packages/features/ee/integration-attribute-sync/repositories/IIntegrationAttributeSyncRepository.ts @@ -63,6 +63,7 @@ export interface ISyncFormData { id: string; name: string; credentialId?: number; + integration?: AttributeSyncIntegrations; enabled: boolean; organizationId: number; ruleId: string; @@ -138,7 +139,7 @@ export interface IIntegrationAttributeSyncUpdateParams { integrationAttributeSync: Omit< IntegrationAttributeSync, "attributeSyncRule" | "syncFieldMappings" | "integration" - >; + > & { integration?: AttributeSyncIntegrations }; attributeSyncRule: AttributeSyncRule; fieldMappingsToCreate: Omit[]; fieldMappingsToUpdate: AttributeSyncFieldMapping[]; diff --git a/packages/features/ee/integration-attribute-sync/services/IntegrationAttributeSyncService.ts b/packages/features/ee/integration-attribute-sync/services/IntegrationAttributeSyncService.ts index 3d3ce371338a97..50e7a1c4cb2bc3 100644 --- a/packages/features/ee/integration-attribute-sync/services/IntegrationAttributeSyncService.ts +++ b/packages/features/ee/integration-attribute-sync/services/IntegrationAttributeSyncService.ts @@ -217,4 +217,11 @@ export class IntegrationAttributeSyncService { async getAllByCredentialId(credentialId: number) { return this.deps.integrationAttributeSyncRepository.getAllByCredentialId(credentialId); } + + async validateCredentialBelongsToOrg(credentialId: number, organizationId: number) { + return this.deps.credentialRepository.findByIdAndTeamId({ + id: credentialId, + teamId: organizationId, + }); + } } diff --git a/packages/trpc/server/routers/viewer/attribute-sync/updateAttributeSync.handler.ts b/packages/trpc/server/routers/viewer/attribute-sync/updateAttributeSync.handler.ts index 207fa6e480f2a3..1ff9b2e49794a1 100644 --- a/packages/trpc/server/routers/viewer/attribute-sync/updateAttributeSync.handler.ts +++ b/packages/trpc/server/routers/viewer/attribute-sync/updateAttributeSync.handler.ts @@ -1,4 +1,8 @@ import { getIntegrationAttributeSyncService } from "@calcom/ee/integration-attribute-sync/di/IntegrationAttributeSyncService.container"; +import { + AttributeSyncIntegrations, + type ISyncFormData, +} from "@calcom/ee/integration-attribute-sync/repositories/IIntegrationAttributeSyncRepository"; import { DuplicateAttributeWithinSyncError, DuplicateAttributeAcrossSyncsError, @@ -37,8 +41,29 @@ const updateAttributeSyncHandler = async ({ ctx, input }: UpdateAttributeSyncOpt throw new TRPCError({ code: "UNAUTHORIZED" }); } + // Never trust user-supplied organizationId — override with authenticated user's org + const safeInput: ISyncFormData = { ...input, organizationId: org.id }; + + // Validate credential belongs to the user's organization and derive integration type + if (input.credentialId !== undefined) { + const credential = await integrationAttributeSyncService.validateCredentialBelongsToOrg( + input.credentialId, + org.id + ); + if (!credential) { + throw new TRPCError({ code: "NOT_FOUND", message: "Credential not found" }); + } + + const integrationValue = credential.app?.slug || credential.type; + if (!Object.values(AttributeSyncIntegrations).includes(integrationValue as AttributeSyncIntegrations)) { + throw new TRPCError({ code: "BAD_REQUEST", message: `Unsupported integration type: ${integrationValue}` }); + } + + safeInput.integration = integrationValue as AttributeSyncIntegrations; + } + try { - await integrationAttributeSyncService.updateIncludeRulesAndMappings(input); + await integrationAttributeSyncService.updateIncludeRulesAndMappings(safeInput); } catch (error) { if (error instanceof DuplicateAttributeWithinSyncError) { throw new TRPCError({ From d25130274f3a9655d1ecab5518a4c5d53113487b Mon Sep 17 00:00:00 2001 From: Pedro Castro Date: Tue, 14 Apr 2026 08:29:46 -0300 Subject: [PATCH 3/5] fix: scope bulk user deletion to callers organization (#28872) --- apps/web/playwright/lib/orgMigration.ts | 1 + packages/features/profile/repositories/ProfileRepository.ts | 4 ++-- .../routers/viewer/organizations/bulkDeleteUsers.handler.ts | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/web/playwright/lib/orgMigration.ts b/apps/web/playwright/lib/orgMigration.ts index 926edeb7ecda81..30bdbb19333152 100644 --- a/apps/web/playwright/lib/orgMigration.ts +++ b/apps/web/playwright/lib/orgMigration.ts @@ -653,6 +653,7 @@ async function dbRemoveUserFromOrg({ await ProfileRepository.deleteMany({ userIds: [userToRemoveFromOrg.id], + organizationId: userToRemoveFromOrg.organizationId!, }); } diff --git a/packages/features/profile/repositories/ProfileRepository.ts b/packages/features/profile/repositories/ProfileRepository.ts index 0f43a10aeb261c..42256d0e4176c5 100644 --- a/packages/features/profile/repositories/ProfileRepository.ts +++ b/packages/features/profile/repositories/ProfileRepository.ts @@ -400,10 +400,10 @@ export class ProfileRepository implements IProfileRepository { }); } - static deleteMany({ userIds }: { userIds: number[] }) { + static deleteMany({ userIds, organizationId }: { userIds: number[]; organizationId: number }) { // Even though there can be just one profile matching a userId and organizationId, we are using deleteMany as it won't error if the profile doesn't exist return prisma.profile.deleteMany({ - where: { userId: { in: userIds } }, + where: { userId: { in: userIds }, organizationId }, }); } diff --git a/packages/trpc/server/routers/viewer/organizations/bulkDeleteUsers.handler.ts b/packages/trpc/server/routers/viewer/organizations/bulkDeleteUsers.handler.ts index da0d993e1e64a4..8668e4d1cff858 100644 --- a/packages/trpc/server/routers/viewer/organizations/bulkDeleteUsers.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/bulkDeleteUsers.handler.ts @@ -82,6 +82,7 @@ export async function bulkDeleteUsersHandler({ ctx, input }: BulkDeleteUsersHand id: { in: input.userIds, }, + organizationId: currentUserOrgId, }, data: { organizationId: null, @@ -130,6 +131,7 @@ export async function bulkDeleteUsersHandler({ ctx, input }: BulkDeleteUsersHand const removeProfiles = ProfileRepository.deleteMany({ userIds: input.userIds, + organizationId: currentUserOrgId, }); // We do this in a transaction to make sure that all memberships are removed before we remove the organization relation from the user From 74936a3abe367948adc6eec42de4e44ca0d9b280 Mon Sep 17 00:00:00 2001 From: Pedro Castro Date: Tue, 14 Apr 2026 08:48:47 -0300 Subject: [PATCH 4/5] fix: align reassignment query handlers with existing access check pattern (#28876) --- .../getManagedEventUsersToReassign.handler.ts | 13 +++++++++++++ .../getRoundRobinHostsToReasign.handler.ts | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/trpc/server/routers/viewer/teams/managedEvents/getManagedEventUsersToReassign.handler.ts b/packages/trpc/server/routers/viewer/teams/managedEvents/getManagedEventUsersToReassign.handler.ts index bc5c60696e6da3..7ccfeeb3f89e03 100644 --- a/packages/trpc/server/routers/viewer/teams/managedEvents/getManagedEventUsersToReassign.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/managedEvents/getManagedEventUsersToReassign.handler.ts @@ -4,6 +4,7 @@ import { ensureAvailableUsers } from "@calcom/features/bookings/lib/handleNewBoo import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB"; import type { IsFixedAwareUser } from "@calcom/features/bookings/lib/handleNewBooking/types"; import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository"; +import { getBookingAccessService } from "@calcom/features/di/containers/BookingAccessService"; import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository"; import { withSelectedCalendars } from "@calcom/features/users/repositories/UserRepository"; import { ErrorCode } from "@calcom/lib/errorCodes"; @@ -12,6 +13,8 @@ import type { PrismaClient } from "@calcom/prisma"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; +import { TRPCError } from "@trpc/server"; + import type { TGetManagedEventUsersToReassignInputSchema } from "./getManagedEventUsersToReassign.schema"; type GetManagedEventUsersToReassignOptions = { @@ -74,6 +77,16 @@ export const getManagedEventUsersToReassign = async ({ prefix: ["gettingManagedEventUsersToReassign", `${bookingId}`], }); + const bookingAccessService = getBookingAccessService(); + const isAllowed = await bookingAccessService.doesUserIdHaveAccessToBooking({ + userId: user.id, + bookingId, + }); + + if (!isAllowed) { + throw new TRPCError({ code: "FORBIDDEN", message: "You do not have permission" }); + } + const bookingRepository = new BookingRepository(prisma); const eventTypeRepository = new EventTypeRepository(prisma); diff --git a/packages/trpc/server/routers/viewer/teams/roundRobin/getRoundRobinHostsToReasign.handler.ts b/packages/trpc/server/routers/viewer/teams/roundRobin/getRoundRobinHostsToReasign.handler.ts index 72ee49149f271d..f72173c25cbaf7 100644 --- a/packages/trpc/server/routers/viewer/teams/roundRobin/getRoundRobinHostsToReasign.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/roundRobin/getRoundRobinHostsToReasign.handler.ts @@ -2,6 +2,7 @@ import { enrichUsersWithDelegationCredentials } from "@calcom/app-store/delegati import dayjs from "@calcom/dayjs"; import { ensureAvailableUsers } from "@calcom/features/bookings/lib/handleNewBooking/ensureAvailableUsers"; import type { IsFixedAwareUser } from "@calcom/features/bookings/lib/handleNewBooking/types"; +import { getBookingAccessService } from "@calcom/features/di/containers/BookingAccessService"; import { withSelectedCalendars } from "@calcom/features/users/repositories/UserRepository"; import { ErrorCode } from "@calcom/lib/errorCodes"; import logger from "@calcom/lib/logger"; @@ -10,6 +11,8 @@ import { userSelect } from "@calcom/prisma"; import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; +import { TRPCError } from "@trpc/server"; + import type { TGetRoundRobinHostsToReassignInputSchema } from "./getRoundRobinHostsToReasign.schema"; type GetRoundRobinHostsToReassignOptions = { @@ -111,6 +114,16 @@ export const getRoundRobinHostsToReassign = async ({ ctx, input }: GetRoundRobin prefix: ["gettingRoundRobinHostsToReassign", `${bookingId}`], }); + const bookingAccessService = getBookingAccessService(); + const isAllowed = await bookingAccessService.doesUserIdHaveAccessToBooking({ + userId: user.id, + bookingId, + }); + + if (!isAllowed) { + throw new TRPCError({ code: "FORBIDDEN", message: "You do not have permission" }); + } + const booking = await prisma.booking.findUniqueOrThrow({ where: { id: bookingId }, select: { From c0d105e7b395f91745fb4301f6575e85dc1e7d52 Mon Sep 17 00:00:00 2001 From: Pedro Castro Date: Tue, 14 Apr 2026 09:12:54 -0300 Subject: [PATCH 5/5] fix: require uid for booking cancellation on web cancel route (#28868) --- apps/web/app/api/cancel/route.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/web/app/api/cancel/route.ts b/apps/web/app/api/cancel/route.ts index 7a2dcfebfa55dc..b1e6b211ad5514 100644 --- a/apps/web/app/api/cancel/route.ts +++ b/apps/web/app/api/cancel/route.ts @@ -22,6 +22,14 @@ async function handler(req: NextRequest) { } const bookingData = bookingCancelWithCsrfSchema.parse(appDirRequestBody); + // Integer IDs are sequential/guessable — only accept high-entropy UIDs on this route + if (!bookingData.uid) { + return NextResponse.json( + { success: false, message: "uid is required for booking cancellation" }, + { status: 400 } + ); + } + const csrfError = await validateCsrfToken(bookingData.csrfToken); if (csrfError) { return csrfError; @@ -38,8 +46,11 @@ async function handler(req: NextRequest) { identifier, }); + // Strip integer id to ensure lookup is always by uid + const { id: _id, ...safeBookingData } = bookingData; + const result = await handleCancelBooking({ - bookingData, + bookingData: safeBookingData, userId: session?.user?.id || -1, userUuid: session?.user?.uuid, actionSource: "WEBAPP",