Security Fixes (#224)

Fixes - 2,3,4 security vulnerabilities reported in this message.
https://calendso.slack.com/archives/C03127U5S5Q/p1671922033089329

More Fixes
- Dont't allow a user to add a random attendee to a booking not owned by
him
- Don't allow a user to add a random cal user as an organizer of the
booking.
- Membership deletion should be as per the Privileges of
Owner,Admin,Member
This commit is contained in:
Hariom Balhara authored and GitHub committed 2023-01-04 15:17:47 -07:00
1 parent 604d937661
commit 9c23a8e5ab
7 files changed
+144 -19

No files matched your search

+15
View File
@@ -1,5 +1,7 @@
import type { NextApiRequest } from "next";
import { z } from "zod";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { schemaAttendeeEditBodyParams, schemaAttendeeReadPublic } from "~/lib/validations/attendee";
@@ -50,8 +52,21 @@ export async function patchHandler(req: NextApiRequest) {
const { prisma, query, body } = req;
const { id } = schemaQueryIdParseInt.parse(query);
const data = schemaAttendeeEditBodyParams.parse(body);
await checkPermissions(req, data);
const attendee = await prisma.attendee.update({ where: { id }, data });
return { attendee: schemaAttendeeReadPublic.parse(attendee) };
}
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaAttendeeEditBodyParams>) {
const { isAdmin, prisma } = req;
if (isAdmin) return;
const { userId } = req;
const { bookingId } = body;
if (bookingId) {
// Ensure that the booking the attendee is being added to belongs to the user
const booking = await prisma.booking.findFirst({ where: { id: bookingId, userId } });
if (!booking) throw new HttpError({ statusCode: 403, message: "You don't have access to the booking" });
}
}
export default defaultResponder(patchHandler);
+14
View File
@@ -1,5 +1,7 @@
import type { NextApiRequest } from "next";
import { z } from "zod";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { schemaBookingEditBodyParams, schemaBookingReadPublic } from "~/lib/validations/booking";
@@ -49,8 +51,20 @@ export async function patchHandler(req: NextApiRequest) {
const { prisma, query, body } = req;
const { id } = schemaQueryIdParseInt.parse(query);
const data = schemaBookingEditBodyParams.parse(body);
await checkPermissions(req, data);
const booking = await prisma.booking.update({ where: { id }, data });
return { booking: schemaBookingReadPublic.parse(booking) };
}
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaBookingEditBodyParams>) {
const { isAdmin } = req;
if (body.userId && !isAdmin) {
// Organizer has to be a cal user and we can't allow a booking to be transfered to some other cal user's name
throw new HttpError({
statusCode: 403,
message: "Only admin can change the organizer of a booking",
});
}
}
export default defaultResponder(patchHandler);
+11 -3
View File
@@ -1,11 +1,18 @@
import type { NextApiRequest } from "next";
import { z } from "zod";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { schemaEventTypeEditBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
import {
schemaEventTypeEditBodyParams,
schemaEventTypeBaseBodyParams,
schemaEventTypeReadPublic,
} from "~/lib/validations/event-type";
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission";
/**
* @swagger
* /event-types/{id}:
@@ -37,18 +44,19 @@ export async function patchHandler(req: NextApiRequest) {
const { prisma, query, body } = req;
const { id } = schemaQueryIdParseInt.parse(query);
const data = schemaEventTypeEditBodyParams.parse(body);
await checkPermissions(req);
await checkPermissions(req, body);
const event_type = await prisma.eventType.update({ where: { id }, data });
return { event_type: schemaEventTypeReadPublic.parse(event_type) };
}
async function checkPermissions(req: NextApiRequest) {
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaEventTypeBaseBodyParams>) {
const { userId, prisma, isAdmin } = req;
const { id } = schemaQueryIdParseInt.parse(req.query);
if (isAdmin) return;
/** Only event type owners can modify it */
const eventType = await prisma.eventType.findFirst({ where: { id, userId } });
if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" });
await checkTeamEventEditPermission(req, body);
}
export default defaultResponder(patchHandler);
+3 -16
View File
@@ -6,6 +6,8 @@ import { defaultResponder } from "@calcom/lib/server";
import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
import checkTeamEventEditPermission from "./_utils/checkTeamEventEditPermission";
/**
* @swagger
* /event-types:
@@ -61,22 +63,7 @@ async function postHandler(req: NextApiRequest) {
if (isAdmin && body.userId) args = { data: { ...body, users: { connect: { id: body.userId } } } };
if (body.teamId) {
const hasMembership = await prisma.membership.findFirst({
where: {
userId,
teamId: body.teamId,
accepted: true,
},
});
if (!hasMembership?.role || !["ADMIN", "OWNER"].includes(hasMembership.role)) {
throw new HttpError({
statusCode: 401,
message: "No permission to create an event-type for this team`",
});
}
}
await checkTeamEventEditPermission(req, body);
const data = await prisma.eventType.create(args);
@@ -0,0 +1,29 @@
import type { NextApiRequest } from "next";
import { z } from "zod";
import { HttpError } from "@calcom/lib/http-error";
import { schemaEventTypeBaseBodyParams } from "~/lib/validations/event-type";
export default async function checkTeamEventEditPermission(
req: NextApiRequest,
body: Pick<z.infer<typeof schemaEventTypeBaseBodyParams>, "teamId">
) {
const { prisma, userId } = req;
if (body.teamId) {
const membership = await prisma.membership.findFirst({
where: {
userId,
teamId: body.teamId,
accepted: true,
},
});
if (!membership?.role || !["ADMIN", "OWNER"].includes(membership.role)) {
throw new HttpError({
statusCode: 401,
message: "No permission to operate on event-type for this team",
});
}
}
}
+60
View File
@@ -1,5 +1,6 @@
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { membershipIdSchema } from "~/lib/validations/membership";
@@ -35,8 +36,67 @@ import { membershipIdSchema } from "~/lib/validations/membership";
export async function deleteHandler(req: NextApiRequest) {
const { prisma, query } = req;
const userId_teamId = membershipIdSchema.parse(query);
await checkPermissions(req);
await prisma.membership.delete({ where: { userId_teamId } });
return { message: `Membership with id: ${query.id} deleted successfully` };
}
async function checkPermissions(req: NextApiRequest) {
const { prisma, isAdmin, userId, query } = req;
const userId_teamId = membershipIdSchema.parse(query);
// Admin User can do anything including deletion of Admin Team Member in any team
if (isAdmin) {
return;
}
// Owner can delete Admin and Member
// Admin Team Member can delete Member
// Member can't delete anyone
const PRIVILEGE_ORDER = ["OWNER", "ADMIN", "MEMBER"];
const memberShipToBeDeleted = await prisma.membership.findUnique({
where: { userId_teamId },
});
if (!memberShipToBeDeleted) {
throw new HttpError({ statusCode: 404, message: "Membership not found" });
}
// If a user is deleting their own membership, then they can do it
if (userId === memberShipToBeDeleted.userId) {
return;
}
const currentUserMembership = await prisma.membership.findUnique({
where: {
userId_teamId: {
userId,
teamId: memberShipToBeDeleted.teamId,
},
},
});
if (!currentUserMembership) {
// Current User isn't a member of the team
throw new HttpError({ statusCode: 403, message: "You are not a member of the team" });
}
if (
PRIVILEGE_ORDER.indexOf(memberShipToBeDeleted.role) === -1 ||
PRIVILEGE_ORDER.indexOf(currentUserMembership.role) === -1
) {
throw new HttpError({ statusCode: 400, message: "Invalid role" });
}
// If Role that is being deleted comes before the current User's Role, or it's the same ROLE, throw error
if (
PRIVILEGE_ORDER.indexOf(memberShipToBeDeleted.role) <= PRIVILEGE_ORDER.indexOf(currentUserMembership.role)
) {
throw new HttpError({
statusCode: 403,
message: "You don't have the appropriate role to delete this membership",
});
}
}
export default defaultResponder(deleteHandler);
+12
View File
@@ -1,5 +1,7 @@
import type { NextApiRequest } from "next";
import { z } from "zod";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { schemaSchedulePublic, schemaSingleScheduleBodyParams } from "~/lib/validations/schedule";
@@ -32,8 +34,18 @@ export async function patchHandler(req: NextApiRequest) {
const { prisma, query } = req;
const { id } = schemaQueryIdParseInt.parse(query);
const data = schemaSingleScheduleBodyParams.parse(req.body);
await checkPermissions(req, data);
const result = await prisma.schedule.update({ where: { id }, data, include: { availability: true } });
return { schedule: schemaSchedulePublic.parse(result) };
}
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaSingleScheduleBodyParams>) {
const { isAdmin } = req;
if (isAdmin) return;
if (body.userId) {
throw new HttpError({ statusCode: 403, message: "Non admin cannot change the owner of a schedule" });
}
//_auth-middleware takes care of verifying the ownership of schedule.
}
export default defaultResponder(patchHandler);