Skip to content

Commit 13103c0

Browse files
authored
feat: Improve error handling in ratelimit (calcom#25223)
* Improve error handling in ratelimit * Update route.ts * Update route.ts * Update route.ts * address feedback
1 parent 983af06 commit 13103c0

6 files changed

Lines changed: 49 additions & 2 deletions

File tree

apps/web/app/api/auth/reset-password/route.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import { z } from "zod";
77

88
import { validPassword } from "@calcom/features/auth/lib/validPassword";
99
import { hashPassword } from "@calcom/lib/auth/hashPassword";
10+
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
11+
import getIP from "@calcom/lib/getIP";
12+
import { piiHasher } from "@calcom/lib/server/PiiHasher";
1013
import prisma from "@calcom/prisma";
1114
import { IdentityProvider } from "@calcom/prisma/enums";
1215

@@ -36,8 +39,14 @@ async function handler(req: NextRequest) {
3639
// token verified, delete the cookie / a resubmit on failure requires a new csrf token.
3740
cookieStore.delete("calcom.csrf_token");
3841

39-
// rate-limited there is a low, very low chance that a password request stays valid long enough
40-
// to brute force 3.8126967e+40 options.
42+
const remoteIp = getIP(req);
43+
await checkRateLimitAndThrowError({
44+
rateLimitingType: "core",
45+
identifier: `api:reset-password:${piiHasher.hash(remoteIp)}`,
46+
});
47+
48+
// Note: There is a low, very low chance that a password request stays valid long enough
49+
// to brute force 3.8126967e+40 options, but rate limiting provides additional protection.
4150
const maybeRequest = await prisma.resetPasswordRequest.findFirstOrThrow({
4251
where: {
4352
id: rawRequestId,

apps/web/app/api/auth/signup/route.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import { NextResponse, type NextRequest } from "next/server";
55
import calcomSignupHandler from "@calcom/feature-auth/signup/handlers/calcomHandler";
66
import selfHostedSignupHandler from "@calcom/feature-auth/signup/handlers/selfHostedHandler";
77
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
8+
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
89
import { IS_PREMIUM_USERNAME_ENABLED } from "@calcom/lib/constants";
910
import getIP from "@calcom/lib/getIP";
1011
import { HttpError } from "@calcom/lib/http-error";
1112
import logger from "@calcom/lib/logger";
13+
import { piiHasher } from "@calcom/lib/server/PiiHasher";
1214
import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken";
1315
import { prisma } from "@calcom/prisma";
1416
import { signupSchema } from "@calcom/prisma/zod-utils";
@@ -38,6 +40,12 @@ async function handler(req: NextRequest) {
3840
const remoteIp = getIP(req);
3941
// Use a try catch instead of returning res every time
4042
try {
43+
// Rate limit: 10 signups per 60 seconds per IP
44+
await checkRateLimitAndThrowError({
45+
rateLimitingType: "core",
46+
identifier: `api:signup:${piiHasher.hash(remoteIp)}`,
47+
});
48+
4149
const body = await parseRequestData(req);
4250
await checkCfTurnstileToken({
4351
token: req.headers.get("cf-access-token") as string,

apps/web/app/api/auth/two-factor/totp/disable/route.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { NextRequest } from "next/server";
77
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
88
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
99
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
10+
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
1011
import { symmetricDecrypt } from "@calcom/lib/crypto";
1112
import { totpAuthenticatorCheck } from "@calcom/lib/totp";
1213
import prisma from "@calcom/prisma";
@@ -27,6 +28,11 @@ async function handler(req: NextRequest) {
2728
return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 });
2829
}
2930

31+
await checkRateLimitAndThrowError({
32+
rateLimitingType: "core",
33+
identifier: `api:totp-disable:${session.user.id}`,
34+
});
35+
3036
const user = await prisma.user.findUnique({ where: { id: session.user.id }, include: { password: true } });
3137

3238
if (!user) {

apps/web/app/api/auth/two-factor/totp/enable/route.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { NextRequest } from "next/server";
66

77
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
88
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
9+
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
910
import { symmetricDecrypt } from "@calcom/lib/crypto";
1011
import { totpAuthenticatorCheck } from "@calcom/lib/totp";
1112
import prisma from "@calcom/prisma";
@@ -25,6 +26,11 @@ async function postHandler(req: NextRequest) {
2526
return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 });
2627
}
2728

29+
await checkRateLimitAndThrowError({
30+
rateLimitingType: "core",
31+
identifier: `api:totp-enable:${session.user.id}`,
32+
});
33+
2834
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
2935
if (!user) {
3036
console.error(`Session references user that no longer exists.`);

apps/web/app/api/auth/two-factor/totp/setup/route.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import qrcode from "qrcode";
1010
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
1111
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
1212
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
13+
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
1314
import { symmetricEncrypt } from "@calcom/lib/crypto";
1415
import prisma from "@calcom/prisma";
1516
import { IdentityProvider } from "@calcom/prisma/enums";
@@ -29,6 +30,11 @@ async function postHandler(req: NextRequest) {
2930
return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 });
3031
}
3132

33+
await checkRateLimitAndThrowError({
34+
rateLimitingType: "core",
35+
identifier: `api:totp-setup:${session.user.id}`,
36+
});
37+
3238
const user = await prisma.user.findUnique({ where: { id: session.user.id }, include: { password: true } });
3339

3440
if (!user) {

apps/web/app/api/cancel/route.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import type { NextRequest } from "next/server";
55

66
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
77
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
8+
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
9+
import getIP from "@calcom/lib/getIP";
10+
import { piiHasher } from "@calcom/lib/server/PiiHasher";
811
import { bookingCancelWithCsrfSchema } from "@calcom/prisma/zod-utils";
912
import { validateCsrfToken } from "@calcom/web/lib/validateCsrfToken";
1013

@@ -26,6 +29,15 @@ async function handler(req: NextRequest) {
2629

2730
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
2831

32+
// Rate limit: 10 booking cancellations per 60 seconds per user (or IP if not authenticated)
33+
const identifier = session?.user?.id
34+
? `api:cancel-user:${session.user.id}`
35+
: `api:cancel-ip:${piiHasher.hash(getIP(req))}`;
36+
await checkRateLimitAndThrowError({
37+
rateLimitingType: "core",
38+
identifier,
39+
});
40+
2941
const result = await handleCancelBooking({
3042
bookingData,
3143
userId: session?.user?.id || -1,

0 commit comments

Comments
 (0)