chore: when using pii for rate limit (email, ip) hash (#22922)

This commit is contained in:
Alex van Andel authored and GitHub committed 2025-08-06 13:54:57 +01:00
1 parent a1fc4bccb9
commit b41031626f
14 files changed
+69 -17

No files matched your search

@@ -7,6 +7,7 @@ import { passwordResetRequest } from "@calcom/features/auth/lib/passwordResetReq
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { emailSchema } from "@calcom/lib/emailSchema";
import prisma from "@calcom/prisma";
import { piiHasher } from "@calcom/lib/server/PiiHasher";
async function handler(req: NextRequest) {
const body = await parseRequestData(req);
@@ -28,7 +29,7 @@ async function handler(req: NextRequest) {
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: ip,
identifier: piiHasher.hash(ip),
});
try {
+2 -1
View File
@@ -7,6 +7,7 @@ import getIP from "@calcom/lib/getIP";
import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import { CreationSource } from "@calcom/prisma/enums";
import { piiHasher } from "@calcom/lib/server/PiiHasher";
async function handler(req: NextApiRequest & { userId?: number }) {
const userIp = getIP(req);
@@ -20,7 +21,7 @@ async function handler(req: NextApiRequest & { userId?: number }) {
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: userIp,
identifier: piiHasher.hash(userIp),
});
const session = await getServerSession({ req });
+2 -1
View File
@@ -6,13 +6,14 @@ import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowE
import getIP from "@calcom/lib/getIP";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import { CreationSource } from "@calcom/prisma/enums";
import { piiHasher } from "@calcom/lib/server/PiiHasher";
async function handler(req: NextApiRequest & { userId?: number }) {
const userIp = getIP(req);
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: `instant.event-${userIp}`,
identifier: `instant.event-${piiHasher.hash(userIp)}`,
});
const session = await getServerSession({ req });
+2 -1
View File
@@ -7,6 +7,7 @@ import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowE
import getIP from "@calcom/lib/getIP";
import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import { piiHasher } from "@calcom/lib/server/PiiHasher";
// @TODO: Didn't look at the contents of this function in order to not break old booking page.
@@ -37,7 +38,7 @@ async function handler(req: NextApiRequest & RequestMeta) {
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: userIp,
identifier: piiHasher.hash(userIp),
});
const session = await getServerSession({ req });
/* To mimic API behavior and comply with types */
@@ -48,6 +48,7 @@ import { dub } from "./dub";
import { isPasswordValid } from "./isPasswordValid";
import CalComAdapter from "./next-auth-custom-adapter";
import { verifyPassword } from "./verifyPassword";
import { hashEmail } from "@calcom/lib/server/PiiHasher";
const log = logger.getSubLogger({ prefix: ["next-auth-options"] });
const GOOGLE_API_CREDENTIALS = process.env.GOOGLE_API_CREDENTIALS || "{}";
@@ -131,7 +132,7 @@ const providers: Provider[] = [
}
await checkRateLimitAndThrowError({
identifier: user.email,
identifier: hashEmail(user.email),
});
if (!user.password?.hash && user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
+3 -2
View File
@@ -13,6 +13,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { getTranslation } from "@calcom/lib/server/i18n";
import { prisma } from "@calcom/prisma";
import { hashEmail } from "@calcom/lib/server/PiiHasher";
const log = logger.getSubLogger({ prefix: [`[[Auth] `] });
@@ -54,7 +55,7 @@ export const sendEmailVerification = async ({
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: email,
identifier: hashEmail(email),
});
await prisma.verificationToken.create({
@@ -142,7 +143,7 @@ export const sendChangeOfEmailVerification = async ({ user, language }: ChangeOf
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: user.emailFrom,
identifier: hashEmail(user.emailFrom),
});
await prisma.verificationToken.create({
+26
View File
@@ -0,0 +1,26 @@
import { describe, it, expect } from "vitest";
import { hashEmail, Md5PiiHasher } from "./PiiHasher";
describe("PII Hasher Test Suite", () => {
const hasher = new Md5PiiHasher("test-salt");
it("can hash email addresses", async () => {
const email = "sensitive_data@example.com";
const hashedEmail = hashEmail(email, hasher);
expect(hashedEmail).toBe("2e74ca9edc8add1709b0d049aa2a0959@example.com");
});
it("can hash PII with saltyMd5", async () => {
const pii = "sensitive_data";
const hashedPii = hasher.hash(pii);
expect(hashedPii).toBe("2e74ca9edc8add1709b0d049aa2a0959");
});
it("handles hashing with different salt", () => {
const differentHasher = new Md5PiiHasher("different-salt");
const pii = "sensitive_data";
const hashedPii = differentHasher.hash(pii);
expect(hashedPii).not.toBe(hasher.hash(pii));
});
});
+20
View File
@@ -0,0 +1,20 @@
import { createHash } from "crypto";
export interface PiiHasher {
hash(input: string): string;
}
export class Md5PiiHasher implements PiiHasher {
constructor(private readonly salt: string) {}
hash(input: string) {
return createHash("md5").update(this.salt + input).digest("hex");
}
}
export const piiHasher: PiiHasher = new Md5PiiHasher(process.env.CALENDSO_ENCRYPTION_KEY!);
export const hashEmail = (email: string, hasher: PiiHasher = piiHasher): string => {
const [localPart, domain] = email.split("@");
// Simple hash function for email, can be replaced with a more complex one if needed
return hasher.hash(localPart) + "@" + domain;
}
+2 -1
View File
@@ -4,6 +4,7 @@ import { sendSmsOrFallbackEmail } from "@calcom/features/ee/workflows/lib/remind
import { checkSMSRateLimit } from "@calcom/lib/checkRateLimitAndThrowError";
import { SENDER_ID } from "@calcom/lib/constants";
import isSmsCalEmail from "@calcom/lib/isSmsCalEmail";
import { piiHasher } from "@calcom/lib/server/PiiHasher";
import { TimeFormat } from "@calcom/lib/timeFormat";
import prisma from "@calcom/prisma";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
@@ -32,7 +33,7 @@ const handleSendingSMS = async ({
? `handleSendingSMS:team:${teamId}`
: organizerUserId
? `handleSendingSMS:user:${organizerUserId}`
: `handleSendingSMS:user:${reminderPhone}`,
: `handleSendingSMS:user:${piiHasher.hash(reminderPhone)}`,
rateLimitingType: "sms",
});
@@ -22,7 +22,7 @@ export const addSecondaryEmailHandler = async ({ ctx, input }: AddSecondaryEmail
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: `addSecondaryEmail.${user.email}`,
identifier: `addSecondaryEmail.${user.id}`,
});
const existingPrimaryEmail = await prisma.user.findUnique({
@@ -21,11 +21,10 @@ const log = logger.getSubLogger({ prefix: [`[[Auth] `] });
export const resendVerifyEmail = async ({ input, ctx }: ResendEmailOptions) => {
let emailToVerify = ctx.user.email;
const identifer = emailToVerify;
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: `resendVerifyEmail.${identifer}`,
identifier: `resendVerifyEmail.${ctx.user.id}`,
});
let emailVerified = Boolean(ctx.user.emailVerified);
@@ -6,6 +6,7 @@ import getIP from "@calcom/lib/getIP";
import type { TRPCContext } from "../../../createContext";
import type { TSendVerifyEmailCodeSchema } from "./sendVerifyEmailCode.schema";
import { hashEmail, piiHasher } from "@calcom/lib/server/PiiHasher";
type SendVerifyEmailCode = {
input: TSendVerifyEmailCodeSchema;
@@ -13,11 +14,11 @@ type SendVerifyEmailCode = {
};
export const sendVerifyEmailCodeHandler = async ({ input, req }: SendVerifyEmailCode) => {
const identifer = req ? getIP(req as NextApiRequest) : input.email;
const identifier = req ? piiHasher.hash(getIP(req as NextApiRequest)) : hashEmail(input.email);
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: `emailVerifyByCode.${identifer}`,
identifier: `emailVerifyByCode.${identifier}`,
});
const email = await sendEmailVerificationByCode({
@@ -8,6 +8,7 @@ import type { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { TRPCError } from "@trpc/server";
import { hashEmail } from "@calcom/lib/server/PiiHasher";
type VerifyCodeOptions = {
ctx: {
@@ -34,7 +35,7 @@ export const verifyCodeHandler = async ({ ctx, input }: VerifyCodeOptions) => {
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: email,
identifier: hashEmail(email),
});
const secret = createHash("md5")
@@ -1,5 +1,4 @@
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import logger from "@calcom/lib/logger";
import { isTeamAdmin, isTeamOwner } from "@calcom/lib/server/queries/teams";
import { TeamService } from "@calcom/lib/server/service/teamService";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
@@ -8,7 +7,6 @@ import { TRPCError } from "@trpc/server";
import type { TRemoveMemberInputSchema } from "./removeMember.schema";
const log = logger.getSubLogger({ prefix: ["viewer/teams/removeMember.handler"] });
type RemoveMemberOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
@@ -19,7 +17,7 @@ type RemoveMemberOptions = {
export const removeMemberHandler = async ({ ctx, input }: RemoveMemberOptions) => {
await checkRateLimitAndThrowError({
identifier: `removeMember.${ctx.sourceIp}`,
identifier: `removeMember.${ctx.user.id}`,
});
const { memberIds, teamIds, isOrg } = input;