diff --git a/apps/web/app/api/auth/reset-password/route.ts b/apps/web/app/api/auth/reset-password/route.ts index 1ebdc5da4b..7dfc308256 100644 --- a/apps/web/app/api/auth/reset-password/route.ts +++ b/apps/web/app/api/auth/reset-password/route.ts @@ -7,6 +7,9 @@ import { z } from "zod"; import { validPassword } from "@calcom/features/auth/lib/validPassword"; import { hashPassword } from "@calcom/lib/auth/hashPassword"; +import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; +import getIP from "@calcom/lib/getIP"; +import { piiHasher } from "@calcom/lib/server/PiiHasher"; import prisma from "@calcom/prisma"; import { IdentityProvider } from "@calcom/prisma/enums"; @@ -36,8 +39,14 @@ async function handler(req: NextRequest) { // token verified, delete the cookie / a resubmit on failure requires a new csrf token. cookieStore.delete("calcom.csrf_token"); - // rate-limited there is a low, very low chance that a password request stays valid long enough - // to brute force 3.8126967e+40 options. + const remoteIp = getIP(req); + await checkRateLimitAndThrowError({ + rateLimitingType: "core", + identifier: `api:reset-password:${piiHasher.hash(remoteIp)}`, + }); + + // Note: There is a low, very low chance that a password request stays valid long enough + // to brute force 3.8126967e+40 options, but rate limiting provides additional protection. const maybeRequest = await prisma.resetPasswordRequest.findFirstOrThrow({ where: { id: rawRequestId, diff --git a/apps/web/app/api/auth/signup/route.ts b/apps/web/app/api/auth/signup/route.ts index 05326592fc..db31e4ab95 100644 --- a/apps/web/app/api/auth/signup/route.ts +++ b/apps/web/app/api/auth/signup/route.ts @@ -5,10 +5,12 @@ import { NextResponse, type NextRequest } from "next/server"; import calcomSignupHandler from "@calcom/feature-auth/signup/handlers/calcomHandler"; import selfHostedSignupHandler from "@calcom/feature-auth/signup/handlers/selfHostedHandler"; import { FeaturesRepository } from "@calcom/features/flags/features.repository"; +import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import { IS_PREMIUM_USERNAME_ENABLED } from "@calcom/lib/constants"; import getIP from "@calcom/lib/getIP"; import { HttpError } from "@calcom/lib/http-error"; import logger from "@calcom/lib/logger"; +import { piiHasher } from "@calcom/lib/server/PiiHasher"; import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken"; import { prisma } from "@calcom/prisma"; import { signupSchema } from "@calcom/prisma/zod-utils"; @@ -38,6 +40,12 @@ async function handler(req: NextRequest) { const remoteIp = getIP(req); // Use a try catch instead of returning res every time try { + // Rate limit: 10 signups per 60 seconds per IP + await checkRateLimitAndThrowError({ + rateLimitingType: "core", + identifier: `api:signup:${piiHasher.hash(remoteIp)}`, + }); + const body = await parseRequestData(req); await checkCfTurnstileToken({ token: req.headers.get("cf-access-token") as string, diff --git a/apps/web/app/api/auth/two-factor/totp/disable/route.ts b/apps/web/app/api/auth/two-factor/totp/disable/route.ts index 952711f51b..85b9f062a6 100644 --- a/apps/web/app/api/auth/two-factor/totp/disable/route.ts +++ b/apps/web/app/api/auth/two-factor/totp/disable/route.ts @@ -7,6 +7,7 @@ import type { NextRequest } from "next/server"; import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword"; +import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import { symmetricDecrypt } from "@calcom/lib/crypto"; import { totpAuthenticatorCheck } from "@calcom/lib/totp"; import prisma from "@calcom/prisma"; @@ -27,6 +28,11 @@ async function handler(req: NextRequest) { return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 }); } + await checkRateLimitAndThrowError({ + rateLimitingType: "core", + identifier: `api:totp-disable:${session.user.id}`, + }); + const user = await prisma.user.findUnique({ where: { id: session.user.id }, include: { password: true } }); if (!user) { diff --git a/apps/web/app/api/auth/two-factor/totp/enable/route.ts b/apps/web/app/api/auth/two-factor/totp/enable/route.ts index 79409e3ae4..4e841b5b0e 100644 --- a/apps/web/app/api/auth/two-factor/totp/enable/route.ts +++ b/apps/web/app/api/auth/two-factor/totp/enable/route.ts @@ -6,6 +6,7 @@ import type { NextRequest } from "next/server"; import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import { symmetricDecrypt } from "@calcom/lib/crypto"; import { totpAuthenticatorCheck } from "@calcom/lib/totp"; import prisma from "@calcom/prisma"; @@ -25,6 +26,11 @@ async function postHandler(req: NextRequest) { return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 }); } + await checkRateLimitAndThrowError({ + rateLimitingType: "core", + identifier: `api:totp-enable:${session.user.id}`, + }); + const user = await prisma.user.findUnique({ where: { id: session.user.id } }); if (!user) { console.error(`Session references user that no longer exists.`); diff --git a/apps/web/app/api/auth/two-factor/totp/setup/route.ts b/apps/web/app/api/auth/two-factor/totp/setup/route.ts index ea665ef1dd..76a206a667 100644 --- a/apps/web/app/api/auth/two-factor/totp/setup/route.ts +++ b/apps/web/app/api/auth/two-factor/totp/setup/route.ts @@ -10,6 +10,7 @@ import qrcode from "qrcode"; import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword"; +import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import { symmetricEncrypt } from "@calcom/lib/crypto"; import prisma from "@calcom/prisma"; import { IdentityProvider } from "@calcom/prisma/enums"; @@ -29,6 +30,11 @@ async function postHandler(req: NextRequest) { return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 }); } + await checkRateLimitAndThrowError({ + rateLimitingType: "core", + identifier: `api:totp-setup:${session.user.id}`, + }); + const user = await prisma.user.findUnique({ where: { id: session.user.id }, include: { password: true } }); if (!user) { diff --git a/apps/web/app/api/cancel/route.ts b/apps/web/app/api/cancel/route.ts index 723c76ee67..d217305b83 100644 --- a/apps/web/app/api/cancel/route.ts +++ b/apps/web/app/api/cancel/route.ts @@ -5,6 +5,9 @@ import type { NextRequest } from "next/server"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking"; +import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; +import getIP from "@calcom/lib/getIP"; +import { piiHasher } from "@calcom/lib/server/PiiHasher"; import { bookingCancelWithCsrfSchema } from "@calcom/prisma/zod-utils"; import { validateCsrfToken } from "@calcom/web/lib/validateCsrfToken"; @@ -26,6 +29,15 @@ async function handler(req: NextRequest) { const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); + // Rate limit: 10 booking cancellations per 60 seconds per user (or IP if not authenticated) + const identifier = session?.user?.id + ? `api:cancel-user:${session.user.id}` + : `api:cancel-ip:${piiHasher.hash(getIP(req))}`; + await checkRateLimitAndThrowError({ + rateLimitingType: "core", + identifier, + }); + const result = await handleCancelBooking({ bookingData, userId: session?.user?.id || -1,